diff --git a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj
index 695e93e2..f474d537 100644
--- a/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj
+++ b/LearningHub.Nhs.WebUI.AutomatedUiTests/LearningHub.Nhs.WebUI.AutomatedUiTests.csproj
@@ -9,10 +9,11 @@
True
+
-
+
diff --git a/LearningHub.Nhs.WebUI/Configuration/Settings.cs b/LearningHub.Nhs.WebUI/Configuration/Settings.cs
index e300e80e..42778aee 100644
--- a/LearningHub.Nhs.WebUI/Configuration/Settings.cs
+++ b/LearningHub.Nhs.WebUI/Configuration/Settings.cs
@@ -265,5 +265,10 @@ public Settings()
/// Gets or sets AllCataloguePageSize.
///
public int AllCataloguePageSize { get; set; }
+
+ ///
+ /// Gets or sets the StatMandId.
+ ///
+ public int StatMandId { get; set; } = 12;
}
}
\ No newline at end of file
diff --git a/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs
new file mode 100644
index 00000000..4ea7d3a5
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Controllers/ReportsController.cs
@@ -0,0 +1,429 @@
+namespace LearningHub.Nhs.WebUI.Controllers
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Net.Http;
+ using System.Threading.Tasks;
+ using Azure;
+ using GDS.MultiPageFormData;
+ using GDS.MultiPageFormData.Enums;
+ using LearningHub.Nhs.Caching;
+ using LearningHub.Nhs.Models.Databricks;
+ using LearningHub.Nhs.Models.Moodle;
+ using LearningHub.Nhs.Models.MyLearning;
+ using LearningHub.Nhs.Models.Paging;
+ using LearningHub.Nhs.WebUI.Configuration;
+ 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.Account;
+ using LearningHub.Nhs.WebUI.Models.DynamicCheckbox;
+ using LearningHub.Nhs.WebUI.Models.Learning;
+ using LearningHub.Nhs.WebUI.Models.Report;
+ using Microsoft.AspNetCore.Authorization;
+ using Microsoft.AspNetCore.Hosting;
+ using Microsoft.AspNetCore.Mvc;
+ using Microsoft.Extensions.Logging;
+ using Microsoft.Extensions.Options;
+ using NHSUKViewComponents.Web.ViewModels;
+
+ ///
+ /// Defines the .
+ ///
+ // [ServiceFilter(typeof(LoginWizardFilter))]
+ [Authorize]
+ [Route("Reports")]
+ public class ReportsController : BaseController
+ {
+ private const int ReportPageSize = 10;
+ private readonly ICacheService cacheService;
+ private readonly ICategoryService categoryService;
+ private readonly IMultiPageFormService multiPageFormService;
+ private readonly IReportService reportService;
+ private readonly IFileService fileService;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// httpClientFactory.
+ /// cacheService.
+ /// multiPageFormService.
+ /// reportService.
+ /// categoryService.
+ /// fileService.
+ /// The hostingEnvironment.
+ /// The logger.
+ /// settings.
+ public ReportsController(IHttpClientFactory httpClientFactory, IWebHostEnvironment hostingEnvironment, ILogger logger, IOptions settings, ICacheService cacheService, IMultiPageFormService multiPageFormService, IReportService reportService, ICategoryService categoryService, IFileService fileService)
+ : base(hostingEnvironment, httpClientFactory, logger, settings.Value)
+ {
+ this.cacheService = cacheService;
+ this.multiPageFormService = multiPageFormService;
+ this.reportService = reportService;
+ this.categoryService = categoryService;
+ this.fileService = fileService;
+ }
+
+ ///
+ /// The Report landing page.
+ ///
+ /// reportHistoryViewModel.
+ /// The .
+ [ResponseCache(CacheProfileName = "Never")]
+ public async Task Index(ReportHistoryViewModel reportHistoryViewModel = null)
+ {
+ int page = 1;
+ this.TempData.Clear();
+ var newReport = new DatabricksRequestModel { Take = ReportPageSize, Skip = 0 };
+
+ await this.multiPageFormService.SetMultiPageFormData(
+ newReport,
+ MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"),
+ this.TempData);
+
+ var historyRequest = new PagingRequestModel
+ {
+ PageSize = ReportPageSize,
+ };
+
+ switch (reportHistoryViewModel.ReportFormActionType)
+ {
+ case ReportFormActionTypeEnum.NextPageChange:
+ reportHistoryViewModel.CurrentPageIndex += 1;
+ break;
+
+ case ReportFormActionTypeEnum.PreviousPageChange:
+ reportHistoryViewModel.CurrentPageIndex -= 1;
+ break;
+ default:
+ reportHistoryViewModel.CurrentPageIndex = 0;
+ break;
+ }
+
+ page = page + reportHistoryViewModel.CurrentPageIndex;
+ historyRequest.Page = page;
+
+ // get a list of report history and send to view
+ var result = await this.reportService.GetReportHistory(historyRequest);
+ if (result != null)
+ {
+ reportHistoryViewModel.TotalCount = result.TotalItemCount;
+ reportHistoryViewModel.ReportHistoryModels = result.Items;
+ }
+
+ this.ViewData["AllCourses"] = await this.GetCoursesAsync();
+ reportHistoryViewModel.ReportPaging = new ReportPagingModel() { CurrentPage = reportHistoryViewModel.CurrentPageIndex, PageSize = ReportPageSize, TotalItems = reportHistoryViewModel.TotalCount, HasItems = reportHistoryViewModel.TotalCount > 0 };
+ return this.View(reportHistoryViewModel);
+ }
+
+ ///
+ /// CreateReportCourseSelection.
+ ///
+ /// searchText.
+ /// A representing the result of the asynchronous operation.
+ [Route("CreateReportCourseSelection")]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task CreateReportCourseSelection(string searchText = "")
+ {
+ var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+ var coursevm = new ReportCreationCourseSelection { SearchText = searchText, Courses = reportCreation.Courses != null ? reportCreation.Courses : new List() };
+ var getCourses = await this.GetCoursesAsync();
+ if (!string.IsNullOrWhiteSpace(searchText))
+ {
+ getCourses = getCourses.Where(x => x.Value.ToLower().Contains(searchText.ToLower())).ToList();
+ }
+
+ if (coursevm.Courses.Count == 0 && !string.IsNullOrWhiteSpace(reportCreation.TimePeriod))
+ {
+ coursevm.Courses = new List { "all" };
+ }
+
+ coursevm.BuildCourses(getCourses);
+ return this.View(coursevm);
+ }
+
+ ///
+ /// CreateReportCourseSelection.
+ ///
+ /// courseSelection.
+ /// A representing the result of the asynchronous operation.
+ [HttpPost]
+ [Route("CreateReportCourseSelection")]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task CreateReportCourseSelection(ReportCreationCourseSelection courseSelection)
+ {
+ var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+
+ if (courseSelection != null)
+ {
+ if (courseSelection.Courses.Any())
+ {
+ reportCreation.Courses = courseSelection.Courses.Contains("all") ? new List() : courseSelection.Courses;
+ await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+ return this.RedirectToAction("CreateReportDateSelection");
+ }
+ }
+
+ this.ModelState.AddModelError("Courses", CommonValidationErrorMessages.CourseRequired);
+ courseSelection.BuildCourses(await this.GetCoursesAsync());
+ courseSelection.Courses = reportCreation.Courses;
+ return this.View("CreateReportCourseSelection", courseSelection);
+ }
+
+ ///
+ /// CreateReportDateSelection.
+ ///
+ /// A representing the result of the asynchronous operation.
+ [Route("CreateReportDateSelection")]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task CreateReportDateSelection()
+ {
+ var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+ var dateVM = new ReportCreationDateSelection();
+ dateVM.TimePeriod = reportCreation.TimePeriod;
+ if (reportCreation.StartDate.HasValue)
+ {
+ dateVM.StartDay = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Day : 0;
+ dateVM.StartMonth = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Month : 0;
+ dateVM.StartYear = reportCreation.StartDate.HasValue ? reportCreation.StartDate.GetValueOrDefault().Year : 0;
+ dateVM.EndDay = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Day : 0;
+ dateVM.EndMonth = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Month : 0;
+ dateVM.EndYear = reportCreation.EndDate.HasValue ? reportCreation.EndDate.GetValueOrDefault().Year : 0;
+ }
+ else
+ {
+ var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel
+ {
+ StartDate = reportCreation.StartDate,
+ EndDate = reportCreation.EndDate,
+ TimePeriod = reportCreation.TimePeriod,
+ Courses = reportCreation.Courses,
+ ReportHistoryId = reportCreation.ReportHistoryId,
+ Take = 1,
+ Skip = 1,
+ });
+
+ DateTime startDate = DateTime.MinValue;
+ var validDate = DateTime.TryParse(result.MinValidDate, out startDate);
+ dateVM.StartDay = validDate ? startDate.Day : 0;
+ dateVM.StartMonth = validDate ? startDate.Month : 0;
+ dateVM.StartYear = validDate ? startDate.Year : 0;
+ }
+
+ return this.View(dateVM);
+ }
+
+ ///
+ /// CreateReportDateSelection.
+ ///
+ /// reportCreationDate.
+ /// A representing the result of the asynchronous operation.
+ [Route("CreateReportSummary")]
+ [HttpPost]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task CreateReportSummary(ReportCreationDateSelection reportCreationDate)
+ {
+ // validate date
+ var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+ reportCreation.TimePeriod = reportCreationDate.TimePeriod;
+ reportCreation.StartDate = reportCreationDate.GetStartDate();
+ reportCreation.EndDate = reportCreationDate.GetEndDate();
+ await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+ return this.RedirectToAction("CourseCompletionReport");
+ }
+
+ ///
+ /// ViewReport.
+ ///
+ /// reportHistoryId.
+ /// A representing the result of the asynchronous operation.
+ [Route("ViewReport/{reportHistoryId}")]
+ [HttpGet]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task ViewReport(int reportHistoryId)
+ {
+ this.TempData.Clear();
+ var report = await this.reportService.GetReportHistoryById(reportHistoryId);
+ if (report == null)
+ {
+ return this.RedirectToAction("Index");
+ }
+
+ var reportRequest = new DatabricksRequestModel { Take = ReportPageSize, Skip = 0 };
+ var periodCheck = int.TryParse(report.PeriodDays.ToString(), out int numberOfDays);
+ if (report.PeriodDays > 0 && periodCheck)
+ {
+ reportRequest.TimePeriod = report.PeriodDays.ToString();
+ reportRequest.StartDate = DateTime.Now.AddDays(-numberOfDays);
+ reportRequest.EndDate = DateTime.Now;
+ }
+ else
+ {
+ reportRequest.TimePeriod = "Custom";
+ reportRequest.StartDate = report.StartDate;
+ reportRequest.EndDate = report.EndDate;
+ reportRequest.ReportHistoryId = reportHistoryId;
+ }
+
+ if (report.CourseFilter == "all")
+ {
+ report.CourseFilter = string.Empty;
+ }
+
+ reportRequest.Courses = report.CourseFilter.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(f => f.Trim()).ToList();
+
+ await this.multiPageFormService.SetMultiPageFormData(reportRequest, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+ return this.RedirectToAction("CourseCompletionReport");
+ }
+
+ ///
+ /// DownloadReport.
+ ///
+ /// reportHistoryId.
+ /// A representing the result of the asynchronous operation.
+ [Route("DownloadReport/{reportHistoryId}")]
+ [HttpGet]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task DownloadReport(int reportHistoryId)
+ {
+ var report = await this.reportService.DownloadReport(reportHistoryId);
+ if (report == null)
+ {
+ return this.RedirectToAction("Index");
+ }
+
+ var result = await this.fileService.DownloadBlobFileAsync(report.FilePath);
+ return this.File(result.Stream, result.ContentType, result.FileName);
+ }
+
+ ///
+ /// ViewReport.
+ ///
+ /// reportHistoryId.
+ /// A representing the result of the asynchronous operation.
+ [Route("QueueReportDownload")]
+ [HttpPost]
+ [ResponseCache(CacheProfileName = "Never")]
+ public async Task QueueReportDownload(int reportHistoryId)
+ {
+ await this.reportService.QueueReportDownload(reportHistoryId);
+ return this.RedirectToAction("CourseCompletionReport");
+ }
+
+ ///
+ /// CourseCompletionReport.
+ ///
+ /// courseCompletion.
+ /// A representing the result of the asynchronous operation.
+ [Route("CourseCompletionReport")]
+ [HttpGet]
+ [ResponseCache(CacheProfileName = "Never")]
+ [TypeFilter(typeof(RedirectMissingMultiPageFormData), Arguments = new object[] { "ReportWizardCWF" })]
+ public async Task CourseCompletionReport(CourseCompletionViewModel courseCompletion = null)
+ {
+ int page = 1;
+
+ // validate date
+ var reportCreation = await this.multiPageFormService.GetMultiPageFormData(MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+
+ switch (courseCompletion.ReportFormActionType)
+ {
+ case ReportFormActionTypeEnum.NextPageChange:
+ courseCompletion.CurrentPageIndex += 1;
+ page = page + courseCompletion.CurrentPageIndex;
+ reportCreation.Skip = courseCompletion.CurrentPageIndex * ReportPageSize;
+ break;
+
+ case ReportFormActionTypeEnum.PreviousPageChange:
+ courseCompletion.CurrentPageIndex -= 1;
+ page = page + courseCompletion.CurrentPageIndex;
+ reportCreation.Skip = courseCompletion.CurrentPageIndex * ReportPageSize;
+ break;
+ default:
+ courseCompletion.CurrentPageIndex = 0;
+ reportCreation.Skip = 0;
+ break;
+ }
+
+ DateTimeOffset today = DateTimeOffset.Now.Date;
+ DateTimeOffset? startDate = null;
+ DateTimeOffset? endDate = null;
+
+ if (int.TryParse(reportCreation.TimePeriod, out int days))
+ {
+ startDate = today.AddDays(-days);
+ endDate = today;
+ }
+ else if (reportCreation.TimePeriod == "Custom")
+ {
+ startDate = reportCreation.StartDate;
+ endDate = reportCreation.EndDate;
+ }
+
+ var result = await this.reportService.GetCourseCompletionReport(new DatabricksRequestModel
+ {
+ StartDate = startDate,
+ EndDate = endDate,
+ TimePeriod = reportCreation.TimePeriod,
+ Courses = reportCreation.Courses,
+ ReportHistoryId = reportCreation.ReportHistoryId,
+ Take = reportCreation.Take,
+ Skip = page,
+ });
+
+ var response = new CourseCompletionViewModel(reportCreation);
+
+ if (result != null)
+ {
+ response.TotalCount = result.TotalCount;
+ response.CourseCompletionRecords = result.CourseCompletionRecords;
+ response.ReportHistoryModel = await this.reportService.GetReportHistoryById(result.ReportHistoryId);
+ reportCreation.ReportHistoryId = result.ReportHistoryId;
+ }
+
+ await this.multiPageFormService.SetMultiPageFormData(reportCreation, MultiPageFormDataFeature.AddCustomWebForm("ReportWizardCWF"), this.TempData);
+
+ var allCourses = await this.GetCoursesAsync();
+
+ List matchedCourseNames;
+
+ if (reportCreation.Courses.Count == 0)
+ {
+ matchedCourseNames = allCourses.Select(course => course.Value).ToList();
+ }
+ else
+ {
+ matchedCourseNames = allCourses
+ .Where(course => reportCreation.Courses.Contains(course.Key))
+ .Select(course => course.Value)
+ .ToList();
+ }
+
+ this.ViewData["matchedCourseNames"] = matchedCourseNames;
+ response.ReportPaging = new ReportPagingModel() { CurrentPage = courseCompletion.CurrentPageIndex, PageSize = ReportPageSize, TotalItems = response.TotalCount, HasItems = response.TotalCount > 0 };
+ return this.View(response);
+ }
+
+ private async Task>> GetCoursesAsync()
+ {
+ int categoryId = this.Settings.StatMandId;
+ var courses = new List>();
+ var subCategories = await this.categoryService.GetCoursesByCategoryIdAsync(categoryId);
+
+ foreach (var subCategory in subCategories.Courses)
+ {
+ courses.Add(new KeyValuePair(subCategory.Id.ToString(), subCategory.Displayname));
+ }
+
+ return courses;
+ }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs
index e41b794f..e763fefa 100644
--- a/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs
+++ b/LearningHub.Nhs.WebUI/Helpers/CommonValidationErrorMessages.cs
@@ -259,5 +259,10 @@ public static class CommonValidationErrorMessages
/// Security question Required.
///
public const string SecurityQuestionRequired = "Please select a security question";
+
+ ///
+ /// Course Required.
+ ///
+ public const string CourseRequired = "Select a course";
}
}
diff --git a/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs b/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs
index eb94d018..8ab50935 100644
--- a/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs
+++ b/LearningHub.Nhs.WebUI/Interfaces/IFileService.cs
@@ -53,5 +53,12 @@ public interface IFileService
/// .
/// The .
Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null, List filePaths = null);
+
+ ///
+ /// The DownloadBlobFile.
+ ///
+ /// uri.
+ /// The .
+ Task<(Stream Stream, string FileName, string ContentType)> DownloadBlobFileAsync(string uri);
}
}
diff --git a/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs b/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs
new file mode 100644
index 00000000..8afa2cbc
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Interfaces/IReportService.cs
@@ -0,0 +1,57 @@
+namespace LearningHub.Nhs.WebUI.Interfaces
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Threading.Tasks;
+ using elfhHub.Nhs.Models.Common;
+ using LearningHub.Nhs.Models.Common;
+ using LearningHub.Nhs.Models.Databricks;
+ using LearningHub.Nhs.Models.Paging;
+
+ ///
+ /// Defines the .
+ ///
+ public interface IReportService
+ {
+ ///
+ /// The GetReporterPermission.
+ ///
+ /// A representing the result of the asynchronous operation.
+ Task GetReporterPermission();
+
+ ///
+ /// The GetCourseCompletionReport.
+ ///
+ /// The requestModel..
+ /// The .
+ Task GetCourseCompletionReport(DatabricksRequestModel requestModel);
+
+ ///
+ /// The GetReportHistory.
+ ///
+ /// The requestModel..
+ /// The .
+ Task> GetReportHistory(PagingRequestModel requestModel);
+
+ ///
+ /// The GetReportHistory.
+ ///
+ /// The reportHistoryId..
+ /// The .
+ Task GetReportHistoryById(int reportHistoryId);
+
+ ///
+ /// The QueueReportDownload.
+ ///
+ /// The reportHistoryId..
+ /// The .
+ Task QueueReportDownload(int reportHistoryId);
+
+ ///
+ /// The DownloadReport.
+ ///
+ /// reportHistoryId.
+ /// The .
+ Task DownloadReport(int reportHistoryId);
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj b/LearningHub.Nhs.WebUI/LearningHub.Nhs.WebUI.csproj
index 39df64b2..adada3f1 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/DynamicCheckbox/DynamicCheckboxItemViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs
new file mode 100644
index 00000000..47cd5ea4
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxItemViewModel.cs
@@ -0,0 +1,28 @@
+namespace LearningHub.Nhs.WebUI.Models.DynamicCheckbox
+{
+ ///
+ /// DynamicCheckboxItemViewModel.
+ ///
+ public class DynamicCheckboxItemViewModel
+ {
+ ///
+ /// Gets or sets a value.
+ ///
+ public string Value { get; set; }
+
+ ///
+ /// Gets or sets a Label.
+ ///
+ public string Label { get; set; }
+
+ ///
+ /// Gets or sets a HintText.
+ ///
+ public string? HintText { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether gets or sets a selected.
+ ///
+ public bool Selected { get; set; }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs
new file mode 100644
index 00000000..85738837
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/DynamicCheckbox/DynamicCheckboxesViewModel.cs
@@ -0,0 +1,45 @@
+namespace LearningHub.Nhs.WebUI.Models.DynamicCheckbox
+{
+ using System.Collections.Generic;
+
+ ///
+ /// DynamicCheckboxesViewModel.
+ ///
+ public class DynamicCheckboxesViewModel
+ {
+ ///
+ /// Gets or sets a Label.
+ ///
+ public string Label { get; set; }
+
+ ///
+ /// Gets or sets a HintText.
+ ///
+ public string HintText { get; set; }
+
+ ///
+ /// Gets or sets a ErrorMessage.
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether gets or sets a Required.
+ ///
+ public bool Required { get; set; }
+
+ ///
+ /// Gets or sets a CssClass.
+ ///
+ public string CssClass { get; set; }
+
+ ///
+ /// Gets or sets SelectedValues.
+ ///
+ public List SelectedValues { get; set; } = [];
+
+ ///
+ /// Gets or sets a Checkboxes.
+ ///
+ public List Checkboxes { get; set; } = [];
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs
index 25b51671..93456496 100644
--- a/LearningHub.Nhs.WebUI/Models/NavigationModel.cs
+++ b/LearningHub.Nhs.WebUI/Models/NavigationModel.cs
@@ -75,6 +75,11 @@ public class NavigationModel
///
public bool ShowBrowseCatalogues { get; set; }
+ ///
+ /// Gets or sets a value indicating whether to show reports.
+ ///
+ public bool ShowReports { get; set; }
+
///
/// Gets or sets a value indicating whether ShowHome.
///
diff --git a/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs b/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs
new file mode 100644
index 00000000..3e183c63
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/Report/CourseCompletionViewModel.cs
@@ -0,0 +1,66 @@
+namespace LearningHub.Nhs.WebUI.Models.Report
+{
+ using System.Collections.Generic;
+ using System.Reflection;
+ using LearningHub.Nhs.Models.Databricks;
+ using LearningHub.Nhs.Models.MyLearning;
+ using LearningHub.Nhs.Models.Paging;
+ using LearningHub.Nhs.WebUI.Models.Learning;
+
+ ///
+ /// CourseCompletionViewModel.
+ ///
+ public class CourseCompletionViewModel : DatabricksRequestModel
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CourseCompletionViewModel()
+ {
+ this.CourseCompletionRecords = new List();
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// DatabricksRequestModel.
+ public CourseCompletionViewModel(DatabricksRequestModel requestModel)
+ {
+ this.CourseCompletionRecords = 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 CurrentPageIndex.
+ ///
+ public int CurrentPageIndex { get; set; } = 0;
+
+ ///
+ /// Gets or sets the TotalCount.
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// Gets or sets the report result paging.
+ ///
+ public PagingViewModel ReportPaging { get; set; }
+
+ ///
+ /// Gets or sets the ReportFormActionTypeEnum.
+ ///
+ public ReportFormActionTypeEnum ReportFormActionType { get; set; }
+
+ ///
+ /// Gets or sets the CourseCompletionRecords.
+ ///
+ public List CourseCompletionRecords { get; set; }
+
+ ///
+ /// Gets or sets the ReportHistoryModel.
+ ///
+ public ReportHistoryModel ReportHistoryModel { get; set; }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs
new file mode 100644
index 00000000..8d760098
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationCourseSelection.cs
@@ -0,0 +1,46 @@
+namespace LearningHub.Nhs.WebUI.Models.Report
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Data;
+ using System.Linq;
+ using LearningHub.Nhs.WebUI.Models.DynamicCheckbox;
+ using NHSUKViewComponents.Web.ViewModels;
+
+ ///
+ /// CourseSelection.
+ ///
+ public class ReportCreationCourseSelection
+ {
+ ///
+ /// Gets or sets the list of courses.
+ ///
+ public List Courses { get; set; }
+
+ ///
+ /// Gets or sets the list of all courses.
+ ///
+ public List AllCources { get; set; }
+
+ ///
+ /// Gets or sets the list of SearchText.
+ ///
+ public string SearchText { get; set; }
+
+ ///
+ /// BuildCourses.
+ ///
+ /// The all Courses.
+ /// The .
+ public List BuildCourses(List> allCourses)
+ {
+ this.AllCources = allCourses.Select(r => new DynamicCheckboxItemViewModel
+ {
+ Value = r.Key.ToString(),
+ Label = r.Value,
+ }).ToList();
+ this.AllCources.Insert(0, new DynamicCheckboxItemViewModel { Value = "all", Label = "All Courses", });
+ return this.AllCources;
+ }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs
new file mode 100644
index 00000000..2263a0aa
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/Report/ReportCreationDateSelection.cs
@@ -0,0 +1,225 @@
+namespace LearningHub.Nhs.WebUI.Models.Report
+{
+ using System;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.Linq;
+ using LearningHub.Nhs.WebUI.Helpers;
+ using NHSUKViewComponents.Web.ViewModels;
+
+ ///
+ /// ReportCreationDateSelection.
+ ///
+ public class ReportCreationDateSelection : IValidatableObject
+ {
+ ///
+ /// Gets or sets the start date to define on the search.
+ ///
+ public string TimePeriod { get; set; }
+
+ ///
+ /// Gets or sets the start date to define on the search.
+ ///
+ ///
+ /// Gets or sets the Day.
+ ///
+ public int? StartDay { get; set; }
+
+ ///
+ /// Gets or sets the end Day.
+ ///
+ public int? EndDay { get; set; }
+
+ ///
+ /// Gets or sets the Country.
+ ///
+ public int? StartMonth { get; set; }
+
+ ///
+ /// Gets or sets the Country.
+ ///
+ public int? EndMonth { get; set; }
+
+ ///
+ /// Gets or sets the Year.
+ ///
+ public int? StartYear { get; set; }
+
+ ///
+ /// Gets or sets the Year.
+ ///
+ public int? EndYear { get; set; }
+
+ ///
+ /// Gets or sets the Year.
+ ///
+ public DateTime? DataStart { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether gets or sets the EndDate.
+ ///
+ public bool EndDate { get; set; }
+
+ ///
+ /// Gets or sets the GetDate.
+ ///
+ /// DateTime.
+ public DateTime? GetStartDate()
+ {
+ return (this.StartDay.HasValue && this.StartMonth.HasValue && this.StartYear.HasValue) ? new DateTime(this.StartYear!.Value, this.StartMonth!.Value, this.StartDay!.Value) : (DateTime?)null;
+ }
+
+ ///
+ /// Gets or sets the GetDate.
+ ///
+ /// DateTime.
+ public DateTime? GetEndDate()
+ {
+ return (this.EndDay.HasValue && this.EndMonth.HasValue && this.EndYear.HasValue) ? new DateTime(this.EndYear!.Value, this.EndMonth!.Value, this.EndDay!.Value) : (DateTime?)null;
+ }
+
+ ///
+ public IEnumerable Validate(ValidationContext validationContext)
+ {
+ var validationResults = new List();
+ if (this.TimePeriod == "Custom")
+ {
+ this.ValidateStartDate(validationResults);
+
+ if (this.EndDate)
+ {
+ this.ValidateEndDate(validationResults);
+ }
+ }
+
+ return validationResults;
+ }
+
+ ///
+ /// Gets or sets the GetValidatedStartDate.
+ ///
+ /// DateTime.
+ public DateTime GetValidatedStartDate()
+ {
+ return new DateTime(this.StartYear!.Value, this.StartMonth!.Value, this.StartDay!.Value);
+ }
+
+ ///
+ /// Gets or sets the GetValidatedEndDate.
+ ///
+ /// DateTime.
+ public DateTime? GetValidatedEndDate()
+ {
+ return this.EndDate
+ ? new DateTime(this.EndYear!.Value, this.EndMonth!.Value, this.EndDay!.Value)
+ : (DateTime?)null;
+ }
+
+ ///
+ /// sets the list of radio region.
+ ///
+ /// The .
+ public List PopulateDateRange()
+ {
+ var radios = new List()
+ {
+ new RadiosItemViewModel("7", "7 days", false, null),
+ new RadiosItemViewModel("30", "30 days", false, null),
+ new RadiosItemViewModel("90", "90 days", false, null),
+ };
+
+ // if (string.IsNullOrWhiteSpace(this.TimePeriod))
+ // {
+ // this.TimePeriod = "Custom";
+ // }
+ return radios;
+ }
+
+ ///
+ // public IEnumerable Validate(ValidationContext validationContext)
+ // {
+ // var results = new List();
+ // if (this.TimePeriod == "dateRange")
+ // {
+ // var startDateValidation = DateValidator.ValidateDate(this.StartDay, this.StartMonth, this.StartYear, "valid start date")
+ // .ToValidationResultList(nameof(this.StartDay), nameof(this.StartMonth), nameof(this.StartYear));
+ // if (startDateValidation.Any())
+ // {
+ // results.AddRange(startDateValidation);
+ // }
+ // var endDateValidation = DateValidator.ValidateDate(this.EndDay, this.EndMonth, this.EndYear, "valid end date")
+ // .ToValidationResultList(nameof(this.EndDay), nameof(this.EndMonth), nameof(this.EndYear));
+ // if (endDateValidation.Any())
+ // {
+ // results.AddRange(endDateValidation);
+ // }
+ // }
+ // return results;
+ // }
+ private void ValidateStartDate(List validationResults)
+ {
+ var startDateValidationResults = DateValidator.ValidateDate(
+ this.StartDay,
+ this.StartMonth,
+ this.StartYear,
+ "Start date",
+ true,
+ false,
+ true)
+ .ToValidationResultList(nameof(this.StartDay), nameof(this.StartMonth), nameof(this.StartYear));
+
+ if (!startDateValidationResults.Any())
+ {
+ this.ValidateStartDateIsAfterDataStart(startDateValidationResults);
+ }
+
+ validationResults.AddRange(startDateValidationResults);
+ }
+
+ private void ValidateStartDateIsAfterDataStart(List startDateValidationResults)
+ {
+ var startDate = this.GetValidatedStartDate();
+
+ if (startDate.AddDays(1) < this.DataStart)
+ {
+ startDateValidationResults.Add(
+ new ValidationResult(
+ "Enter a start date after the start of data in the platform", new[] { nameof(this.StartDay), }));
+ startDateValidationResults.Add(
+ new ValidationResult(
+ string.Empty,
+ new[] { nameof(this.StartMonth), nameof(this.StartYear), }));
+ }
+ }
+
+ private void ValidateEndDate(List validationResults)
+ {
+ var endDateValidationResults = DateValidator.ValidateDate(
+ this.EndDay,
+ this.EndMonth,
+ this.EndYear,
+ "End date",
+ true,
+ false,
+ true)
+ .ToValidationResultList(nameof(this.EndDay), nameof(this.EndMonth), nameof(this.EndYear));
+
+ this.ValidateEndDateIsAfterStartDate(endDateValidationResults);
+
+ validationResults.AddRange(endDateValidationResults);
+ }
+
+ private void ValidateEndDateIsAfterStartDate(List endDateValidationResults)
+ {
+ if (this.StartYear > this.EndYear
+ || (this.StartYear == this.EndYear && this.StartMonth > this.EndMonth)
+ || (this.StartYear == this.EndYear && this.StartMonth == this.EndMonth && this.StartDay > this.EndDay))
+ {
+ endDateValidationResults.Add(
+ new ValidationResult("Enter an end date after the start date", new[] { nameof(this.EndDay), }));
+ endDateValidationResults.Add(
+ new ValidationResult(string.Empty, new[] { nameof(this.EndMonth), nameof(this.EndYear), }));
+ }
+ }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs
new file mode 100644
index 00000000..2e38defc
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/Report/ReportFormActionTypeEnum.cs
@@ -0,0 +1,23 @@
+namespace LearningHub.Nhs.WebUI.Models.Learning
+{
+ ///
+ /// Defines the ReportFormActionTypeEnum.
+ ///
+ public enum ReportFormActionTypeEnum
+ {
+ ///
+ /// Defines the basic search for mylearning
+ ///
+ BasicSearch = 0,
+
+ ///
+ /// Previoous page change.
+ ///
+ PreviousPageChange = 1,
+
+ ///
+ /// Next page change.
+ ///
+ NextPageChange = 2,
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs
new file mode 100644
index 00000000..a958c39f
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/Report/ReportHistoryViewModel.cs
@@ -0,0 +1,38 @@
+namespace LearningHub.Nhs.WebUI.Models.Report
+{
+ using System.Collections.Generic;
+ using LearningHub.Nhs.Models.Databricks;
+ using LearningHub.Nhs.Models.Paging;
+ using LearningHub.Nhs.WebUI.Models.Learning;
+
+ ///
+ /// ReportHistoryViewModel.
+ ///
+ public class ReportHistoryViewModel
+ {
+ ///
+ /// Gets or sets the CurrentPageIndex.
+ ///
+ public int CurrentPageIndex { get; set; } = 0;
+
+ ///
+ /// Gets or sets the TotalCount.
+ ///
+ public int TotalCount { get; set; }
+
+ ///
+ /// Gets or sets the report result paging.
+ ///
+ public PagingViewModel ReportPaging { get; set; }
+
+ ///
+ /// Gets or sets the ReportFormActionTypeEnum.
+ ///
+ public ReportFormActionTypeEnum ReportFormActionType { get; set; }
+
+ ///
+ /// Gets or sets the ReportHistoryModels.
+ ///
+ public List ReportHistoryModels { get; set; }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs b/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs
new file mode 100644
index 00000000..5468bab7
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Models/Report/ReportPagingModel.cs
@@ -0,0 +1,20 @@
+namespace LearningHub.Nhs.WebUI.Models.Learning
+{
+ using LearningHub.Nhs.Models.Paging;
+
+ ///
+ /// Defines the .
+ ///
+ public class ReportPagingModel : PagingViewModel
+ {
+ ///
+ /// Gets or sets the page previous action value.
+ ///
+ public int PreviousActionValue { get; set; }
+
+ ///
+ /// Gets or sets the page next action value.
+ ///
+ public int NextActionValue { get; set; }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Program.cs b/LearningHub.Nhs.WebUI/Program.cs
index c24d9057..dda754ec 100644
--- a/LearningHub.Nhs.WebUI/Program.cs
+++ b/LearningHub.Nhs.WebUI/Program.cs
@@ -35,6 +35,19 @@
builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
builder.Host.UseNLog();
+ string corsMoodleUrl = builder.Configuration.GetValue("MoodleAPIConfig:BaseUrl");
+
+ builder.Services.AddCors(options =>
+ {
+ options.AddPolicy("MoodleCORS", builder =>
+ {
+ builder.WithOrigins(corsMoodleUrl.TrimEnd('/'))
+ .AllowAnyHeader()
+ .AllowAnyMethod()
+ .AllowCredentials();
+ });
+ });
+
builder.Services.AddHostedService();
builder.Services.ConfigureServices(builder.Configuration, builder.Environment);
@@ -80,6 +93,8 @@
app.UseRouting();
+ app.UseCors("MoodleCORS");
+
app.UseAuthentication();
app.UseAuthorization();
diff --git a/LearningHub.Nhs.WebUI/Services/FileService.cs b/LearningHub.Nhs.WebUI/Services/FileService.cs
index a09195d6..fda4a044 100644
--- a/LearningHub.Nhs.WebUI/Services/FileService.cs
+++ b/LearningHub.Nhs.WebUI/Services/FileService.cs
@@ -5,6 +5,7 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+ using Azure.Storage.Blobs;
using Azure.Storage.Files.Shares;
using Azure.Storage.Files.Shares.Models;
using Azure.Storage.Sas;
@@ -12,6 +13,7 @@
using LearningHub.Nhs.WebUI.Configuration;
using LearningHub.Nhs.WebUI.Interfaces;
using LearningHub.Nhs.WebUI.Models;
+ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
@@ -249,6 +251,26 @@ public async Task PurgeResourceFile(ResourceVersionExtendedViewModel vm = null,
}
}
+ ///
+ /// The DownloadBlobFile.
+ ///
+ /// url.
+ /// The .
+ public async Task<(Stream Stream, string FileName, string ContentType)> DownloadBlobFileAsync(string url)
+ {
+ var uri = new Uri(url);
+ string containerName = uri.Segments[1].TrimEnd('/');
+ string blobName = string.Join(string.Empty, uri.Segments, 2, uri.Segments.Length - 2);
+ BlobClient blobClient = new BlobClient(this.settings.AzureBlobSettings.ConnectionString, containerName, blobName);
+
+ var properties = await blobClient.GetPropertiesAsync();
+ string contentType = properties.Value.ContentType ?? "application/octet-stream";
+ string fileName = Path.GetFileName(blobClient.Name);
+ var stream = await blobClient.OpenReadAsync();
+
+ return (stream, fileName, contentType);
+ }
+
private static async Task WaitForCopyAsync(ShareFileClient fileClient)
{
// Wait for the copy operation to complete
diff --git a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs
index 7c357ada..bae61e8e 100644
--- a/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs
+++ b/LearningHub.Nhs.WebUI/Services/NavigationPermissionService.cs
@@ -12,18 +12,22 @@ public class NavigationPermissionService : INavigationPermissionService
{
private readonly IResourceService resourceService;
private readonly IUserGroupService userGroupService;
+ private readonly IReportService reportService;
///
/// Initializes a new instance of the class.
///
/// Resource service.
/// UserGroup service.
+ /// Report Service.
public NavigationPermissionService(
IResourceService resourceService,
- IUserGroupService userGroupService)
+ IUserGroupService userGroupService,
+ IReportService reportService)
{
this.resourceService = resourceService;
this.userGroupService = userGroupService;
+ this.reportService = reportService;
}
///
@@ -87,6 +91,7 @@ public NavigationModel NotAuthenticated()
ShowSignOut = false,
ShowMyAccount = false,
ShowBrowseCatalogues = false,
+ ShowReports = false,
};
}
@@ -113,6 +118,7 @@ private NavigationModel AuthenticatedAdministrator(string controllerName)
ShowSignOut = true,
ShowMyAccount = true,
ShowBrowseCatalogues = true,
+ ShowReports = true,
};
}
@@ -139,6 +145,7 @@ private async Task AuthenticatedBlueUser(string controllerName)
ShowSignOut = true,
ShowMyAccount = true,
ShowBrowseCatalogues = true,
+ ShowReports = await this.reportService.GetReporterPermission(),
};
}
@@ -164,6 +171,7 @@ private NavigationModel AuthenticatedGuest()
ShowSignOut = true,
ShowMyAccount = false,
ShowBrowseCatalogues = false,
+ ShowReports = false,
};
}
@@ -190,6 +198,7 @@ private async Task AuthenticatedReadOnly(string controllerName)
ShowSignOut = true,
ShowMyAccount = false,
ShowBrowseCatalogues = true,
+ ShowReports = await this.reportService.GetReporterPermission(),
};
}
@@ -215,6 +224,7 @@ private async Task AuthenticatedBasicUserOnly()
ShowSignOut = true,
ShowMyAccount = true,
ShowBrowseCatalogues = true,
+ ShowReports = false,
};
}
@@ -240,6 +250,7 @@ private NavigationModel InLoginWizard()
ShowSignOut = true,
ShowMyAccount = false,
ShowBrowseCatalogues = false,
+ ShowReports = false,
};
}
}
diff --git a/LearningHub.Nhs.WebUI/Services/ReportService.cs b/LearningHub.Nhs.WebUI/Services/ReportService.cs
new file mode 100644
index 00000000..de383e4d
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Services/ReportService.cs
@@ -0,0 +1,194 @@
+namespace LearningHub.Nhs.WebUI.Services
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Net.Http;
+ using System.Text;
+ using System.Threading.Tasks;
+ using elfhHub.Nhs.Models.Common;
+ using LearningHub.Nhs.Models.Common;
+ using LearningHub.Nhs.Models.Databricks;
+ using LearningHub.Nhs.Models.Paging;
+ using LearningHub.Nhs.Models.Validation;
+ using LearningHub.Nhs.WebUI.Interfaces;
+ using Microsoft.Extensions.Logging;
+ using Newtonsoft.Json;
+
+ ///
+ /// Defines the .
+ ///
+ public class ReportService : BaseService, IReportService
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Web Api Http Client.
+ /// The Open Api Http Client.
+ /// logger.
+ public ReportService(ILearningHubHttpClient learningHubHttpClient, IOpenApiHttpClient openApiHttpClient, ILogger logger)
+ : base(learningHubHttpClient, openApiHttpClient, logger)
+ {
+ }
+
+ ///
+ /// The GetAllAsync.
+ ///
+ /// The .
+ public async Task GetReporterPermission()
+ {
+ bool viewmodel = false;
+
+ var client = await this.OpenApiHttpClient.GetClientAsync();
+
+ var request = $"Report/GetReporterPermission";
+ var response = await client.GetAsync(request).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;
+ }
+
+ ///
+ /// The GetCourseCompletionReport.
+ ///
+ /// The requestModel..
+ /// The .
+ public async Task GetCourseCompletionReport(DatabricksRequestModel requestModel)
+ {
+ DatabricksDetailedViewModel apiResponse = null;
+ var json = JsonConvert.SerializeObject(requestModel);
+ var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
+
+ var client = await this.OpenApiHttpClient.GetClientAsync();
+
+ var request = $"Report/GetCourseCompletionReport";
+ var response = await client.PostAsync(request, stringContent).ConfigureAwait(false);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var result = response.Content.ReadAsStringAsync().Result;
+ apiResponse = JsonConvert.DeserializeObject(result);
+ }
+ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ throw new Exception("AccessDenied");
+ }
+
+ return apiResponse;
+ }
+
+ ///
+ /// The GetReportHistory.
+ ///
+ /// The requestModel..
+ /// The .
+ public async Task> GetReportHistory(PagingRequestModel requestModel)
+ {
+ PagedResultSet apiResponse = null;
+ var json = JsonConvert.SerializeObject(requestModel);
+ var stringContent = new StringContent(json, UnicodeEncoding.UTF8, "application/json");
+
+ var client = await this.OpenApiHttpClient.GetClientAsync();
+
+ var request = $"Report/GetReportHistory";
+ var response = await client.PostAsync(request, stringContent).ConfigureAwait(false);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var result = response.Content.ReadAsStringAsync().Result;
+ apiResponse = JsonConvert.DeserializeObject>(result);
+ }
+ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ throw new Exception("AccessDenied");
+ }
+
+ return apiResponse;
+ }
+
+ ///
+ /// The GetReportHistory.
+ ///
+ /// The reportHistoryId..
+ /// The .
+ public async Task GetReportHistoryById(int reportHistoryId)
+ {
+ ReportHistoryModel apiResponse = null;
+ var client = await this.OpenApiHttpClient.GetClientAsync();
+ var request = $"Report/GetReportHistoryById/{reportHistoryId}";
+ var response = await client.GetAsync(request).ConfigureAwait(false);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var result = response.Content.ReadAsStringAsync().Result;
+ apiResponse = JsonConvert.DeserializeObject(result);
+ }
+ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ throw new Exception("AccessDenied");
+ }
+
+ return apiResponse;
+ }
+
+ ///
+ /// The QueueReportDownload.
+ ///
+ /// The reportHistoryId..
+ /// The .
+ public async Task QueueReportDownload(int reportHistoryId)
+ {
+ bool apiResponse = false;
+ var client = await this.OpenApiHttpClient.GetClientAsync();
+ var request = $"Report/QueueReportDownload/{reportHistoryId}";
+ var response = await client.GetAsync(request).ConfigureAwait(false);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var result = response.Content.ReadAsStringAsync().Result;
+ apiResponse = JsonConvert.DeserializeObject(result);
+ }
+ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ throw new Exception("AccessDenied");
+ }
+
+ return apiResponse;
+ }
+
+ ///
+ /// The DownloadReport.
+ ///
+ /// reportHistoryId.
+ /// The .
+ public async Task DownloadReport(int reportHistoryId)
+ {
+ ReportHistoryModel apiResponse = null;
+ var client = await this.OpenApiHttpClient.GetClientAsync();
+ var request = $"Report/DownloadReport/{reportHistoryId}";
+ var response = await client.GetAsync(request).ConfigureAwait(false);
+
+ if (response.IsSuccessStatusCode)
+ {
+ var result = response.Content.ReadAsStringAsync().Result;
+ apiResponse = JsonConvert.DeserializeObject(result);
+ }
+ else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized || response.StatusCode == System.Net.HttpStatusCode.Forbidden)
+ {
+ throw new Exception("AccessDenied");
+ }
+
+ return apiResponse;
+ }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs
index 6ed25835..26f75aeb 100644
--- a/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs
+++ b/LearningHub.Nhs.WebUI/Startup/ServiceMappings.cs
@@ -81,6 +81,7 @@ public static void AddLearningHubMappings(this IServiceCollection services, ICon
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
diff --git a/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss
new file mode 100644
index 00000000..0f6d9990
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Styles/nhsuk/pages/reporting.scss
@@ -0,0 +1,56 @@
+@use "../../abstracts/all" as *;
+
+
+.user-report {
+ .nhsuk-details__summary {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .override-summary-color {
+ color: black !important;
+ font-weight: normal;
+ text-decoration: none;
+ }
+
+ .nhsuk-summary-list__key--tight {
+ flex: 0 0 20%;
+ width: auto;
+ }
+
+ .nhsuk-summary-list__row {
+ display: flex !important;
+ align-items: flex-start; /* optional: aligns top of key/value */
+ }
+
+ .nhsuk-summary-list__row {
+ border-bottom: none;
+ }
+
+ .nhsuk-summary-list__key, .nhsuk-summary-list__value, .nhsuk-summary-list__actions {
+ border: none;
+ }
+
+ .nhsuk-date-inline {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem; /* space between label and input */
+ }
+
+ .nhsuk-date-inline .nhsuk-label {
+ margin-bottom: 0;
+ white-space: nowrap;
+ }
+
+ .nhsuk-button--with-border {
+ border: 2px solid #005eb8;
+ background-color: #ffffff;
+ color: #005eb8;
+ }
+
+ .nhsuk-button--with-border:hover {
+ background-color: #f0f8ff;
+ border-color: #003087;
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs b/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs
new file mode 100644
index 00000000..01ef484a
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/ViewComponents/DynamicCheckboxesViewComponent.cs
@@ -0,0 +1,58 @@
+namespace LearningHub.Nhs.WebUI.ViewComponents
+{
+ using System.Collections.Generic;
+ using System.Linq;
+ using LearningHub.Nhs.WebUI.Models.DynamicCheckbox;
+ using Microsoft.AspNetCore.Mvc;
+
+ ///
+ /// Defines the .
+ ///
+ public class DynamicCheckboxesViewComponent : ViewComponent
+ {
+ ///
+ /// The Invoke.
+ ///
+ /// label.
+ /// checkboxes.
+ /// required.
+ /// errorMessage.
+ /// hintText.
+ /// cssClass.
+ /// selectedValues.
+ /// propertyName.
+ /// A representing the result of the synchronous operation.
+ public IViewComponentResult Invoke(
+ string label,
+ IEnumerable checkboxes,
+ bool required = false,
+ string? errorMessage = null,
+ string? hintText = null,
+ string? cssClass = null,
+ IEnumerable? selectedValues = null,
+ string propertyName = "SelectedValues")
+ {
+ var selectedList = selectedValues?.ToList() ?? new List();
+
+ var viewModel = new DynamicCheckboxesViewModel
+ {
+ Label = label,
+ HintText = string.IsNullOrWhiteSpace(hintText) ? null : hintText,
+ ErrorMessage = errorMessage,
+ Required = required,
+ CssClass = string.IsNullOrWhiteSpace(cssClass) ? null : cssClass,
+ SelectedValues = selectedList,
+ Checkboxes = checkboxes.Select(cb => new DynamicCheckboxItemViewModel
+ {
+ Value = cb.Value,
+ Label = cb.Label,
+ HintText = cb.HintText,
+ Selected = selectedList.Contains(cb.Value),
+ }).ToList(),
+ };
+
+ this.ViewData["PropertyName"] = propertyName;
+ return this.View(viewModel);
+ }
+ }
+}
diff --git a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml
index 1b37e297..af85c380 100644
--- a/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml
+++ b/LearningHub.Nhs.WebUI/Views/Policies/AcceptableUsePolicy.cshtml
@@ -92,10 +92,13 @@
4.5.22 contain harmful material;
4.5.23 give the impression that the Contribution emanates from us, if this is not the case; or
4.5.24 disclose any third party’s confidential information, identity, personally identifiable information or personal data (including data concerning health).
-
4.6 You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive, indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.
-
4.7 When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).
4.6 Posts must be made with consideration of the NHS People Promise[https://www.england.nhs.uk/our-nhs-people/online-version/lfaop/our-nhs-people-promise/].
+
4.7 You acknowledge and accept that, when using the Platform and accessing the Content, some Content that has been uploaded by third parties may be factually inaccurate, or the topics addressed by the Content may be offensive,indecent, or objectionable in nature. We are not responsible (legally or otherwise) for any claim you may have in relation to the Content.
+
4.8 When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources of evidence are valid (for example, by peer review).
5 Metadata
When making any Contribution, you must where prompted include a sufficient description of the Content so that other users can understand the description, source, and age of the Content. For example, if Content has been quality assured, then the relevant information should be posted in the appropriate field. All metadata fields on the Platform must be completed appropriately before initiating upload. Including the correct information is important in order to help other users locate the Content (otherwise the Content may not appear in search results for others to select).
+
When producing Content to upload to the Platform, we encourage you to implement NICE guideline recommendations and ensure that sources evidence are valid (for example, by peer review).
6 Updates
You must update each Contribution at least once every 3 (three) years, or update or remove it should it cease to be relevant or become outdated or revealed or generally perceived to be unsafe or otherwise unsuitable for inclusion on the Platform.
7 Accessibility
@@ -121,13 +124,13 @@
10.4 legal proceedings against you for reimbursement of all costs on an indemnity basis (including, but not limited to, reasonable administrative and legal costs) resulting from the breach, and/or further legal action against you;
10.5 disclosure of such information to law enforcement authorities as we reasonably feel is necessary or as required by law; and/or
10.6 any other action we reasonably deem appropriate.
-
Moderation of Contributions
+
11 Moderation of Contributions
11.1 Course Manager means a person authorised by NHS England who is responsible for creating and managing courses and learning resources on the Learning Hub, including the moderation of learner's Contributions within social learning environments of an online course, such as discussion forums.
11.2 Course Managers shall be responsible for monitoring local forum activity and ensuring compliance of all Contributions with the Learning Hub’s Acceptable Use Policy. Inappropriate Contributions shall be addressed promptly and escalated where necessary to the Learning Hub, who will review the user’s access to the Platform and the relevant forum.
11.3 Course Managers shall also take responsibility for the following:
-
11.3.1 Clear Expectations: Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.
-
11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.
-
11.3.3 Data Protection and Safety: Ensure that no sensitive or personal data i shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.
+
11.3.1Clear Expectations:Set clear local forum rules within their course at the outset, compliantly with this Acceptable Use Policy, which includes expected behaviour, response times and moderation practices.
+
11.3.2 Inclusive Practice: Encourage participation from all learners/users and foster a respectful, inclusive environment that values diverse perspectives. This should involve reminding local forum users of the appropriate measures of this Acceptable Use Policy and potentially removing Contributions that infringe the rules of this Acceptable Use Policy including under paragraph 4.5 above.
+
11.3.3 Data Protection and Safety:Ensure that no sensitive or personal data is shared, requested or stored in local forums, including by promptly removing any inclusions or requests for such material and notifying NHS England promptly of any potential breaches by contacting NHS England’s Data Protection Officer team via england.dpo@nhs.net; and ensure that risks are actively managed and the local forums are maintained as a safe space for discussion.
11.4 All users must comply with this Acceptable Use Policy. Breaches may result in removal of Contributions and/or withdrawal or suspension of user access without notice.
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml
new file mode 100644
index 00000000..9677805f
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Views/Reports/CourseCompletionReport.cshtml
@@ -0,0 +1,135 @@
+@using LearningHub.Nhs.Models.Enums
+@using LearningHub.Nhs.Models.Enums.Report
+@using LearningHub.Nhs.WebUI.Helpers
+@using LearningHub.Nhs.WebUI.Models
+@using LearningHub.Nhs.WebUI.Models.Learning
+@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel
+
+@{
+ ViewData["Title"] = "Course completion report";
+ var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}";
+
+
+ var pagingModel = Model.ReportPaging;
+ int currentPage = pagingModel.CurrentPage;
+ int pageSize = pagingModel.PageSize;
+ int totalRows = pagingModel.TotalItems;
+
+ int startRow = (currentPage * pageSize) + 1;
+ int endRow = Math.Min(startRow + pageSize - 1, totalRows);
+ var distinctCourses = this.ViewData["matchedCourseNames"] as List;
+
+}
+
+@section styles {
+
+}
+
+
+ You haven’t run any reports yet, so this section is currently empty.
+ Once you generate a report, it will appear here for you to view or download.
+
+
+
+
+
+
+ }
+
+
+
+
+
+
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml
new file mode 100644
index 00000000..5fe25a28
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportPaging.cshtml
@@ -0,0 +1,76 @@
+@using System.Web;
+@using LearningHub.Nhs.WebUI.Models.Learning
+@using LearningHub.Nhs.WebUI.Models.Search;
+@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel
+
+
+@{
+ var pagingModel = Model.ReportPaging;
+ var showPaging = pagingModel.CurrentPage >= 0 && pagingModel.CurrentPage <= pagingModel.TotalPages - 1;
+ var previousMessage = $"{pagingModel.CurrentPage} of {pagingModel.TotalPages}";
+ int CurrentPageNumber = pagingModel.CurrentPage + 1;
+ var nextMessage = string.Empty;
+ if (CurrentPageNumber <= pagingModel.TotalPages)
+ {
+ nextMessage = $"{CurrentPageNumber + 1} of {pagingModel.TotalPages}";
+ }
+ else
+ {
+ previousMessage = $"{CurrentPageNumber - 1} of {pagingModel.TotalPages}";
+ nextMessage = $"{CurrentPageNumber} of {pagingModel.TotalPages}";
+ }
+
+ var routeData = new Dictionary();
+ routeData["CurrentPageIndex"] = pagingModel.CurrentPage.ToString();
+ var nextRouteData = new Dictionary(routeData);
+ var previousRouteData = new Dictionary(routeData);
+ nextRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.NextPageChange.ToString();
+ previousRouteData["ReportFormActionType"] = ReportFormActionTypeEnum.PreviousPageChange.ToString();
+}
+
+@if (pagingModel.TotalPages > 1)
+{
+
+}
\ No newline at end of file
diff --git a/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml
new file mode 100644
index 00000000..492abdc0
--- /dev/null
+++ b/LearningHub.Nhs.WebUI/Views/Reports/_ReportTable.cshtml
@@ -0,0 +1,145 @@
+@model LearningHub.Nhs.WebUI.Models.Report.CourseCompletionViewModel
+
+@{
+ var returnUrl = $"{Context.Request.Path}{Context.Request.QueryString}";
+}
+
+
+@if (Model.TotalCount > 0)
+{
+