diff --git a/DigitalLearningSolutions.Data.Tests/TestHelpers/CentreTestHelper.cs b/DigitalLearningSolutions.Data.Tests/TestHelpers/CentreTestHelper.cs index 65f972fa50..eba6df05a4 100644 --- a/DigitalLearningSolutions.Data.Tests/TestHelpers/CentreTestHelper.cs +++ b/DigitalLearningSolutions.Data.Tests/TestHelpers/CentreTestHelper.cs @@ -35,7 +35,10 @@ public static Centre GetDefaultCentre int ccLicenceSpots = 0, int trainerSpots = 0, string? ipPrefix = "194.176.105", - string? contractType = "Basic" + string? contractType = "Basic", + int customCourses = 0, + long serverSpaceUsed = 0, + long serverSpaceBytes = 0 ) { return new Centre @@ -68,7 +71,10 @@ public static Centre GetDefaultCentre CcLicenceSpots = ccLicenceSpots, TrainerSpots = trainerSpots, IpPrefix = ipPrefix, - ContractType = contractType + ContractType = contractType, + CustomCourses = customCourses, + ServerSpaceBytes = serverSpaceBytes, + ServerSpaceUsed = serverSpaceUsed }; } diff --git a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs index 9cfcfbd0a7..a31614795a 100644 --- a/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs +++ b/DigitalLearningSolutions.Data/DataServices/CentresDataService.cs @@ -1,244 +1,247 @@ -namespace DigitalLearningSolutions.Data.DataServices -{ - using System; - using System.Collections.Generic; - using System.Data; - using Dapper; - using DigitalLearningSolutions.Data.Models; - using DigitalLearningSolutions.Data.Models.DbModels; - using Microsoft.Extensions.Logging; - - public interface ICentresDataService - { - string? GetBannerText(int centreId); - string? GetCentreName(int centreId); - IEnumerable<(int, string)> GetActiveCentresAlphabetical(); - Centre? GetCentreDetailsById(int centreId); - - void UpdateCentreManagerDetails( - int centreId, - string firstName, - string lastName, - string email, - string? telephone - ); - - void UpdateCentreWebsiteDetails( - int centreId, - string postcode, - bool showOnMap, - double latitude, - double longitude, - string? telephone, - string email, - string? openingHours, - string? webAddress, - string? organisationsCovered, - string? trainingVenues, - string? otherInformation - ); - - (string firstName, string lastName, string email) GetCentreManagerDetails(int centreId); - string[] GetCentreIpPrefixes(int centreId); +namespace DigitalLearningSolutions.Data.DataServices +{ + using System; + using System.Collections.Generic; + using System.Data; + using Dapper; + using DigitalLearningSolutions.Data.Models; + using DigitalLearningSolutions.Data.Models.DbModels; + using Microsoft.Extensions.Logging; + + public interface ICentresDataService + { + string? GetBannerText(int centreId); + string? GetCentreName(int centreId); + IEnumerable<(int, string)> GetActiveCentresAlphabetical(); + Centre? GetCentreDetailsById(int centreId); + + void UpdateCentreManagerDetails( + int centreId, + string firstName, + string lastName, + string email, + string? telephone + ); + + void UpdateCentreWebsiteDetails( + int centreId, + string postcode, + bool showOnMap, + double latitude, + double longitude, + string? telephone, + string email, + string? openingHours, + string? webAddress, + string? organisationsCovered, + string? trainingVenues, + string? otherInformation + ); + + (string firstName, string lastName, string email) GetCentreManagerDetails(int centreId); + string[] GetCentreIpPrefixes(int centreId); (bool autoRegistered, string? autoRegisterManagerEmail) GetCentreAutoRegisterValues(int centreId); - IEnumerable GetCentreRanks(DateTime dateSince, int? regionId, int resultsCount, int centreId); - } - - public class CentresDataService : ICentresDataService - { - private readonly IDbConnection connection; - private readonly ILogger logger; - - public CentresDataService(IDbConnection connection, ILogger logger) - { - this.connection = connection; - this.logger = logger; - } - - public string? GetBannerText(int centreId) - { - return connection.QueryFirstOrDefault( - @"SELECT BannerText - FROM Centres - WHERE CentreID = @centreId", - new { centreId } - ); - } - - public string? GetCentreName(int centreId) - { - var name = connection.QueryFirstOrDefault( - @"SELECT CentreName - FROM Centres - WHERE CentreID = @centreId", - new { centreId } - ); - if (name == null) - { - logger.LogWarning - ( - $"No centre found for centre id {centreId}" - ); - } - - return name; - } - - public IEnumerable<(int, string)> GetActiveCentresAlphabetical() - { - var centres = connection.Query<(int, string)> - ( - @"SELECT CentreID, CentreName - FROM Centres - WHERE Active = 1 - ORDER BY CentreName" - ); - return centres; - } - - public Centre? GetCentreDetailsById(int centreId) - { - var centre = connection.QueryFirstOrDefault( - @"SELECT c.CentreID, - c.CentreName, - c.RegionID, - r.RegionName, - c.NotifyEmail, - c.BannerText, - c.SignatureImage, - c.CentreLogo, - c.ContactForename, - c.ContactSurname, - c.ContactEmail, - c.ContactTelephone, - c.pwTelephone AS CentreTelephone, - c.pwEmail AS CentreEmail, - c.pwPostCode AS CentrePostcode, - c.ShowOnMap, - c.Long AS Longitude, - c.Lat AS Latitude, - c.pwHours AS OpeningHours, - c.pwWebURL AS CentreWebAddress, - c.pwTrustsCovered AS OrganisationsCovered, - c.pwTrainingLocations AS TrainingVenues, - c.pwGeneralInfo AS OtherInformation, - c.CMSAdministrators AS CmsAdministratorSpots, - c.CMSManagers AS CmsManagerSpots, - c.CCLicences AS CcLicenceSpots, - c.Trainers AS TrainerSpots, - c.IPPrefix, - ct.ContractType - FROM Centres AS c - INNER JOIN Regions AS r ON r.RegionID = c.RegionID - INNER JOIN ContractTypes AS ct ON ct.ContractTypeID = c.ContractTypeId - WHERE CentreID = @centreId", - new { centreId } - ); - - if (centre == null) - { - logger.LogWarning($"No centre found for centre id {centreId}"); - return null; - } - - if (centre.CentreLogo?.Length < 10) - { - centre.CentreLogo = null; - } - - return centre; - } - - public void UpdateCentreManagerDetails( - int centreId, - string firstName, - string lastName, - string email, - string? telephone - ) - { - connection.Execute( - @"UPDATE Centres SET - ContactForename = @firstName, - ContactSurname = @lastName, - ContactEmail = @email, - ContactTelephone = @telephone - WHERE CentreId = @centreId", - new { firstName, lastName, email, telephone, centreId } - ); - } - - public void UpdateCentreWebsiteDetails( - int centreId, - string postcode, - bool showOnMap, - double latitude, - double longitude, - string? telephone = null, - string? email = null, - string? openingHours = null, - string? webAddress = null, - string? organisationsCovered = null, - string? trainingVenues = null, - string? otherInformation = null - ) - { - connection.Execute( - @"UPDATE Centres SET - pwTelephone = @telephone, - pwEmail = @email, - pwPostCode = @postcode, - showOnMap = @showOnMap, - lat = @latitude, - long = @longitude, - pwHours = @openingHours, - pwWebURL = @webAddress, - pwTrustsCovered = @organisationsCovered, - pwTrainingLocations = @trainingVenues, - pwGeneralInfo = @otherInformation - WHERE CentreId = @centreId", - new - { - telephone, - email, - postcode, - showOnMap, - longitude, - latitude, - openingHours, - webAddress, - organisationsCovered, - trainingVenues, - otherInformation, - centreId - } - ); - } - - public (string firstName, string lastName, string email) GetCentreManagerDetails(int centreId) - { - var info = connection.QueryFirstOrDefault<(string, string, string)>( - @"SELECT ContactForename, ContactSurname, ContactEmail - FROM Centres - WHERE CentreID = @centreId", - new { centreId } - ); - return info; - } - - public string[] GetCentreIpPrefixes(int centreId) - { - var ipPrefixString = connection.QueryFirstOrDefault( - @"SELECT IPPrefix - FROM Centres - WHERE CentreID = @centreId", - new { centreId } - ); - - var ipPrefixes = ipPrefixString?.Split(',', StringSplitOptions.RemoveEmptyEntries); - return ipPrefixes ?? new string[0]; - } - + IEnumerable GetCentreRanks(DateTime dateSince, int? regionId, int resultsCount, int centreId); + } + + public class CentresDataService : ICentresDataService + { + private readonly IDbConnection connection; + private readonly ILogger logger; + + public CentresDataService(IDbConnection connection, ILogger logger) + { + this.connection = connection; + this.logger = logger; + } + + public string? GetBannerText(int centreId) + { + return connection.QueryFirstOrDefault( + @"SELECT BannerText + FROM Centres + WHERE CentreID = @centreId", + new { centreId } + ); + } + + public string? GetCentreName(int centreId) + { + var name = connection.QueryFirstOrDefault( + @"SELECT CentreName + FROM Centres + WHERE CentreID = @centreId", + new { centreId } + ); + if (name == null) + { + logger.LogWarning + ( + $"No centre found for centre id {centreId}" + ); + } + + return name; + } + + public IEnumerable<(int, string)> GetActiveCentresAlphabetical() + { + var centres = connection.Query<(int, string)> + ( + @"SELECT CentreID, CentreName + FROM Centres + WHERE Active = 1 + ORDER BY CentreName" + ); + return centres; + } + + public Centre? GetCentreDetailsById(int centreId) + { + var centre = connection.QueryFirstOrDefault( + @"SELECT c.CentreID, + c.CentreName, + c.RegionID, + r.RegionName, + c.NotifyEmail, + c.BannerText, + c.SignatureImage, + c.CentreLogo, + c.ContactForename, + c.ContactSurname, + c.ContactEmail, + c.ContactTelephone, + c.pwTelephone AS CentreTelephone, + c.pwEmail AS CentreEmail, + c.pwPostCode AS CentrePostcode, + c.ShowOnMap, + c.Long AS Longitude, + c.Lat AS Latitude, + c.pwHours AS OpeningHours, + c.pwWebURL AS CentreWebAddress, + c.pwTrustsCovered AS OrganisationsCovered, + c.pwTrainingLocations AS TrainingVenues, + c.pwGeneralInfo AS OtherInformation, + c.CMSAdministrators AS CmsAdministratorSpots, + c.CMSManagers AS CmsManagerSpots, + c.CCLicences AS CcLicenceSpots, + c.Trainers AS TrainerSpots, + c.IPPrefix, + ct.ContractType, + c.CustomCourses, + c.ServerSpaceUsed, + c.ServerSpaceBytes + FROM Centres AS c + INNER JOIN Regions AS r ON r.RegionID = c.RegionID + INNER JOIN ContractTypes AS ct ON ct.ContractTypeID = c.ContractTypeId + WHERE CentreID = @centreId", + new { centreId } + ); + + if (centre == null) + { + logger.LogWarning($"No centre found for centre id {centreId}"); + return null; + } + + if (centre.CentreLogo?.Length < 10) + { + centre.CentreLogo = null; + } + + return centre; + } + + public void UpdateCentreManagerDetails( + int centreId, + string firstName, + string lastName, + string email, + string? telephone + ) + { + connection.Execute( + @"UPDATE Centres SET + ContactForename = @firstName, + ContactSurname = @lastName, + ContactEmail = @email, + ContactTelephone = @telephone + WHERE CentreId = @centreId", + new { firstName, lastName, email, telephone, centreId } + ); + } + + public void UpdateCentreWebsiteDetails( + int centreId, + string postcode, + bool showOnMap, + double latitude, + double longitude, + string? telephone = null, + string? email = null, + string? openingHours = null, + string? webAddress = null, + string? organisationsCovered = null, + string? trainingVenues = null, + string? otherInformation = null + ) + { + connection.Execute( + @"UPDATE Centres SET + pwTelephone = @telephone, + pwEmail = @email, + pwPostCode = @postcode, + showOnMap = @showOnMap, + lat = @latitude, + long = @longitude, + pwHours = @openingHours, + pwWebURL = @webAddress, + pwTrustsCovered = @organisationsCovered, + pwTrainingLocations = @trainingVenues, + pwGeneralInfo = @otherInformation + WHERE CentreId = @centreId", + new + { + telephone, + email, + postcode, + showOnMap, + longitude, + latitude, + openingHours, + webAddress, + organisationsCovered, + trainingVenues, + otherInformation, + centreId + } + ); + } + + public (string firstName, string lastName, string email) GetCentreManagerDetails(int centreId) + { + var info = connection.QueryFirstOrDefault<(string, string, string)>( + @"SELECT ContactForename, ContactSurname, ContactEmail + FROM Centres + WHERE CentreID = @centreId", + new { centreId } + ); + return info; + } + + public string[] GetCentreIpPrefixes(int centreId) + { + var ipPrefixString = connection.QueryFirstOrDefault( + @"SELECT IPPrefix + FROM Centres + WHERE CentreID = @centreId", + new { centreId } + ); + + var ipPrefixes = ipPrefixString?.Split(',', StringSplitOptions.RemoveEmptyEntries); + return ipPrefixes ?? new string[0]; + } + public (bool autoRegistered, string? autoRegisterManagerEmail) GetCentreAutoRegisterValues(int centreId) { return connection.QueryFirstOrDefault<(bool, string?)>( @@ -249,44 +252,44 @@ FROM Centres ); } - public IEnumerable GetCentreRanks( - DateTime dateSince, - int? regionId, - int resultsCount, - int centreId - ) - { - return connection.Query( - @"WITH SessionsCount AS - ( - SELECT - Count(c.CentreID) AS DelegateSessionCount, - c.CentreID - FROM [Sessions] s - INNER JOIN Candidates c ON s.CandidateID = c.CandidateID - INNER JOIN Centres ct ON c.CentreID = ct.CentreID - WHERE - s.LoginTime > @dateSince - AND c.CentreID <> 101 AND c.CentreID <> 374 - AND (ct.RegionID = @RegionID OR @RegionID IS NULL) - GROUP BY c.CentreID - ), - Rankings AS - ( - SELECT - RANK() OVER (ORDER BY sc.DelegateSessionCount DESC) AS Ranking, - c.CentreID, - c.CentreName, - sc.DelegateSessionCount - FROM SessionsCount sc - INNER JOIN Centres c ON sc.CentreID = c.CentreID - ) - SELECT * - FROM Rankings - WHERE Ranking <= @resultsCount OR CentreID = @centreId - ORDER BY Ranking", - new { dateSince, regionId, resultsCount, centreId } - ); - } - } -} + public IEnumerable GetCentreRanks( + DateTime dateSince, + int? regionId, + int resultsCount, + int centreId + ) + { + return connection.Query( + @"WITH SessionsCount AS + ( + SELECT + Count(c.CentreID) AS DelegateSessionCount, + c.CentreID + FROM [Sessions] s + INNER JOIN Candidates c ON s.CandidateID = c.CandidateID + INNER JOIN Centres ct ON c.CentreID = ct.CentreID + WHERE + s.LoginTime > @dateSince + AND c.CentreID <> 101 AND c.CentreID <> 374 + AND (ct.RegionID = @RegionID OR @RegionID IS NULL) + GROUP BY c.CentreID + ), + Rankings AS + ( + SELECT + RANK() OVER (ORDER BY sc.DelegateSessionCount DESC) AS Ranking, + c.CentreID, + c.CentreName, + sc.DelegateSessionCount + FROM SessionsCount sc + INNER JOIN Centres c ON sc.CentreID = c.CentreID + ) + SELECT * + FROM Rankings + WHERE Ranking <= @resultsCount OR CentreID = @centreId + ORDER BY Ranking", + new { dateSince, regionId, resultsCount, centreId } + ); + } + } +} diff --git a/DigitalLearningSolutions.Data/Models/Centre.cs b/DigitalLearningSolutions.Data/Models/Centre.cs index a232b26305..f0d7a7fcea 100644 --- a/DigitalLearningSolutions.Data/Models/Centre.cs +++ b/DigitalLearningSolutions.Data/Models/Centre.cs @@ -31,5 +31,8 @@ public class Centre public int TrainerSpots { get; set; } public string? IpPrefix { get; set; } public string? ContractType { get; set; } + public int CustomCourses { get; set; } + public long ServerSpaceUsed { get; set; } + public long ServerSpaceBytes { get; set; } } } diff --git a/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs b/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs index 56031a56f5..d7f3235836 100644 --- a/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs +++ b/DigitalLearningSolutions.Web.AutomatedUiTests/AccessibilityTests/BasicAccessibilityTests.cs @@ -29,6 +29,7 @@ public void Page_has_no_accessibility_errors(string url, string pageTitle) [InlineData("/TrackingSystem/Centre/Administrators", "Centre administrators")] [InlineData("/TrackingSystem/Centre/Dashboard", "Centre dashboard")] [InlineData("/TrackingSystem/Centre/Ranking", "Centre ranking")] + [InlineData("/TrackingSystem/Centre/ContractDetails", "Contract details")] [InlineData("/TrackingSystem/CentreConfiguration", "Centre configuration")] [InlineData("/TrackingSystem/CentreConfiguration/EditCentreManagerDetails", "Edit centre manager details")] [InlineData("/TrackingSystem/CentreConfiguration/EditCentreWebsiteDetails", "Edit centre content on DLS website")] diff --git a/DigitalLearningSolutions.Web.Tests/Helpers/DisplayColourHelperTests.cs b/DigitalLearningSolutions.Web.Tests/Helpers/DisplayColourHelperTests.cs new file mode 100644 index 0000000000..f52ab2e2c9 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/Helpers/DisplayColourHelperTests.cs @@ -0,0 +1,79 @@ +namespace DigitalLearningSolutions.Web.Tests.Helpers +{ + using DigitalLearningSolutions.Web.Helpers; + using FluentAssertions; + using NUnit.Framework; + + public class DisplayColourHelperTests + { + [Test] + public void GetDisplayColourForPercentage_returns_grey_for_zero_of_zero() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(0, 0); + + // Then + result.Should().BeEquivalentTo("grey"); + } + + [Test] + public void GetDisplayColourForPercentage_returns_blue_for_no_limit() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(10, -1); + + // Then + result.Should().BeEquivalentTo("blue"); + } + + [Test] + public void GetDisplayColourForPercentage_returns_blue_for_no_limit_with_no_value() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(0, -1); + + // Then + result.Should().BeEquivalentTo("blue"); + } + + [Test] + public void GetDisplayColourForPercentage_returns_green_for_under_sixty_percent() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(59, 100); + + // Then + result.Should().BeEquivalentTo("green"); + } + + [Test] + public void GetDisplayColourForPercentage_returns_yellow_for_sixty_percent() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(60, 100); + + // Then + result.Should().BeEquivalentTo("yellow"); + } + + [Test] + public void GetDisplayColourForPercentage_returns_red_for_one_hundred_percent() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(100, 100); + + // Then + result.Should().BeEquivalentTo("red"); + } + + [Test] + public void GetDisplayColourForPercentage_returns_red_for_zero_limit_with_value() + { + // When + var result = DisplayColourHelper.GetDisplayColourForPercentage(100, 0); + + // Then + result.Should().BeEquivalentTo("red"); + } + } +} diff --git a/DigitalLearningSolutions.Web.Tests/Helpers/DisplayStringHelperTests.cs b/DigitalLearningSolutions.Web.Tests/Helpers/DisplayStringHelperTests.cs new file mode 100644 index 0000000000..0379118d26 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/Helpers/DisplayStringHelperTests.cs @@ -0,0 +1,85 @@ +namespace DigitalLearningSolutions.Web.Tests.Helpers +{ + using System; + using DigitalLearningSolutions.Web.Helpers; + using FluentAssertions; + using NUnit.Framework; + + public class DisplayStringHelperTests + { + private const long Gibibyte = 1073741824; + + [Test] + public void GenerateNumberWithLimitDisplayString_returns_expected_string_with_limit() + { + // When + var result = DisplayStringHelper.FormatNumberWithLimit(1, 5); + + // Then + result.Should().Be("1 / 5"); + } + + [Test] + public void GenerateNumberWithLimitDisplayString_returns_expected_string_with_no_limit() + { + // When + var result = DisplayStringHelper.FormatNumberWithLimit(1, -1); + + // Then + result.Should().Be("1"); + } + + [Test] + public void FormatBytesWithLimit_returns_expected_string_for_bytes() + { + // When + var result = DisplayStringHelper.FormatBytesWithLimit(12, 120); + + // Then + result.Should().Be("12B / 120B"); + } + + [Test] + public void FormatBytesWithLimit_returns_expected_string_for_kilobytes() + { + // When + var result = DisplayStringHelper.FormatBytesWithLimit(12, 1200); + + // Then + result.Should().Be("12B / 1.2KiB"); + } + + [Test] + public void FormatBytesWithLimit_returns_expected_string_for_gibibytes() + { + // When + var result = DisplayStringHelper.FormatBytesWithLimit(12, Gibibyte); + + // Then + result.Should().Be("12B / 1GiB"); + } + + [Test] + public void FormatBytesWithLimit_returns_expected_string_when_less_than_next_size() + { + // Given + var bytes = Gibibyte - 10; + + // When + var result = DisplayStringHelper.FormatBytesWithLimit(12, bytes); + + // Then + result.Should().Be("12B / 1024MiB"); + } + + [Test] + public void FormatBytesWithLimit_throws_exception_with_negative_bytes() + { + // When + Action action = () => DisplayStringHelper.FormatBytesWithLimit(-1, 0); + + // Then + action.Should().Throw(); + } + } +} diff --git a/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Centre/ContractDetailsViewModelTests.cs b/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Centre/ContractDetailsViewModelTests.cs new file mode 100644 index 0000000000..f1f5efd5a0 --- /dev/null +++ b/DigitalLearningSolutions.Web.Tests/ViewModels/TrackingSystem/Centre/ContractDetailsViewModelTests.cs @@ -0,0 +1,153 @@ +namespace DigitalLearningSolutions.Web.Tests.ViewModels.TrackingSystem.Centre +{ + using System.Collections.Generic; + using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Data.Tests.TestHelpers; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.ContractDetails; + using FluentAssertions; + using NUnit.Framework; + + public class ContractDetailsViewModelTests + { + [Test] + public void AdminUsers_and_Centre_populate_expected_values() + { + // Given + var centre = CentreTestHelper.GetDefaultCentre( + cmsAdministratorSpots: 3, + cmsManagerSpots: 13, + ccLicenceSpots: 14, + trainerSpots: 15, + customCourses: 12, + serverSpaceUsed: 1024, + serverSpaceBytes: 1073741824 + ); + var adminUsersAtCentre = GetAdminUsersForTest(); + + // When + var viewModel = new ContractDetailsViewModel(adminUsersAtCentre, centre, 10); + + // Then + viewModel.Administrators.Should().Be("7"); + viewModel.Supervisors.Should().Be("6"); + viewModel.CmsAdministrators.Should().Be("4 / 3"); + viewModel.CmsAdministratorsColour.Should().Be("red"); + viewModel.CmsManagers.Should().Be("1 / 13"); + viewModel.CmsManagersColour.Should().Be("green"); + viewModel.ContentCreators.Should().Be("2 / 14"); + viewModel.ContentCreatorsColour.Should().Be("green"); + viewModel.Trainers.Should().Be("1 / 15"); + viewModel.TrainersColour.Should().Be("green"); + viewModel.CustomCourses.Should().Be("10 / 12"); + viewModel.CustomCoursesColour.Should().Be("yellow"); + viewModel.ServerSpace.Should().Be("1KiB / 1GiB"); + viewModel.ServerSpaceColour.Should().Be("green"); + } + + [Test] + public void AdminUsers_and_Centre_populate_expected_values_with_no_limit() + { + // Given + var centre = CentreTestHelper.GetDefaultCentre( + cmsAdministratorSpots: -1, + cmsManagerSpots: -1, + ccLicenceSpots: -1, + trainerSpots: -1, + customCourses: 12, + serverSpaceUsed: 0, + serverSpaceBytes: 0 + ); + var adminUsersAtCentre = GetAdminUsersForTest(); + + // When + var viewModel = new ContractDetailsViewModel(adminUsersAtCentre, centre, 10); + + // Then + viewModel.Administrators.Should().Be("7"); + viewModel.Supervisors.Should().Be("6"); + viewModel.CmsAdministrators.Should().Be("4"); + viewModel.CmsAdministratorsColour.Should().Be("blue"); + viewModel.CmsManagers.Should().Be("1"); + viewModel.CmsManagersColour.Should().Be("blue"); + viewModel.ContentCreators.Should().Be("2"); + viewModel.ContentCreatorsColour.Should().Be("blue"); + viewModel.Trainers.Should().Be("1"); + viewModel.TrainersColour.Should().Be("blue"); + viewModel.CustomCourses.Should().Be("10 / 12"); + viewModel.CustomCoursesColour.Should().Be("yellow"); + viewModel.ServerSpace.Should().Be("0B / 0B"); + viewModel.ServerSpaceColour.Should().Be("grey"); + } + + private List GetAdminUsersForTest() + { + return new List + { + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: true, + isContentManager: true, + importOnly: true, + isContentCreator: false, + isTrainer: true + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: true, + isContentManager: true, + importOnly: false, + isContentCreator: true, + isTrainer: false + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: true, + isContentManager: false, + importOnly: true, + isContentCreator: true, + isTrainer: false + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: true, + isContentManager: true, + importOnly: true, + isContentCreator: false, + isTrainer: false + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: true, + isContentManager: true, + importOnly: true, + isContentCreator: false, + isTrainer: false + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: true, + isContentManager: true, + importOnly: false, + isContentCreator: false, + isTrainer: false + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: true, + isSupervisor: false, + isContentManager: false, + importOnly: false, + isContentCreator: false, + isTrainer: false + ), + UserTestHelper.GetDefaultAdminUser( + isCentreAdmin: false, + isSupervisor: false, + isContentManager: false, + importOnly: false, + isContentCreator: false, + isTrainer: false + ) + }; + } + } +} diff --git a/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Dashboard/ContractDetailsController.cs b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Dashboard/ContractDetailsController.cs new file mode 100644 index 0000000000..3c5b0e7eb4 --- /dev/null +++ b/DigitalLearningSolutions.Web/Controllers/TrackingSystem/Centre/Dashboard/ContractDetailsController.cs @@ -0,0 +1,40 @@ +namespace DigitalLearningSolutions.Web.Controllers.TrackingSystem.Centre.Dashboard +{ + using DigitalLearningSolutions.Data.DataServices; + using DigitalLearningSolutions.Web.Helpers; + using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.ContractDetails; + using Microsoft.AspNetCore.Authorization; + using Microsoft.AspNetCore.Mvc; + + [Authorize(Policy = CustomPolicies.UserCentreAdmin)] + [Route("/TrackingSystem/Centre/ContractDetails")] + public class ContractDetailsController : Controller + { + private readonly ICentresDataService centresDataService; + private readonly ICourseDataService courseDataService; + private readonly IUserDataService userDataService; + + public ContractDetailsController( + ICentresDataService centresDataService, + IUserDataService userDataService, + ICourseDataService courseDataService + ) + { + this.centresDataService = centresDataService; + this.userDataService = userDataService; + this.courseDataService = courseDataService; + } + + public IActionResult Index() + { + var centreId = User.GetCentreId(); + var centreDetails = centresDataService.GetCentreDetailsById(centreId)!; + var adminUsersAtCentre = userDataService.GetAdminUsersByCentreId(centreId); + var numberOfCourses = courseDataService.GetNumberOfActiveCoursesAtCentreForCategory(centreId, 0); + + var model = new ContractDetailsViewModel(adminUsersAtCentre, centreDetails, numberOfCourses); + + return View(model); + } + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/DisplayColourHelper.cs b/DigitalLearningSolutions.Web/Helpers/DisplayColourHelper.cs new file mode 100644 index 0000000000..e66a2d334c --- /dev/null +++ b/DigitalLearningSolutions.Web/Helpers/DisplayColourHelper.cs @@ -0,0 +1,38 @@ +namespace DigitalLearningSolutions.Web.Helpers +{ + public static class DisplayColourHelper + { + // The colour strings here correspond to css classes defined in contractDetails.scss + public static string GetDisplayColourForPercentage(long number, long limit) + { + if (limit == 0 && number == 0) + { + return "grey"; + } + + if (limit < 0) + { + return "blue"; + } + + var usage = (double)number / limit; + + if (0 <= usage && usage < 0.6) + { + return "green"; + } + + if (0.6 <= usage && usage < 1) + { + return "yellow"; + } + + if (usage >= 1) + { + return "red"; + } + + return "blue"; + } + } +} diff --git a/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs b/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs new file mode 100644 index 0000000000..5071e48ed7 --- /dev/null +++ b/DigitalLearningSolutions.Web/Helpers/DisplayStringHelper.cs @@ -0,0 +1,37 @@ +namespace DigitalLearningSolutions.Web.Helpers +{ + using System; + + public static class DisplayStringHelper + { + private const string Divider = " / "; + private static readonly string[] Units = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB" }; + + public static string FormatNumberWithLimit(int number, int limit) + { + return limit == -1 ? number.ToString() : number + Divider + limit; + } + + public static string FormatBytesWithLimit(long number, long limit) + { + return GenerateBytesDisplayString(number) + Divider + GenerateBytesDisplayString(limit); + } + + private static string GenerateBytesDisplayString(long byteCount) + { + if (byteCount < 0) + { + throw new ArgumentOutOfRangeException(nameof(byteCount), $"Byte count cannot be negative: {byteCount}"); + } + + if (byteCount == 0) + { + return 0 + Units[0]; + } + + var place = Convert.ToInt32(Math.Floor(Math.Log(byteCount, 1024))); + var number = Math.Round(byteCount / Math.Pow(1024, place), 1); + return (Math.Sign(byteCount) * number) + Units[place]; + } + } +} diff --git a/DigitalLearningSolutions.Web/Styles/trackingSystem/contractDetails.scss b/DigitalLearningSolutions.Web/Styles/trackingSystem/contractDetails.scss new file mode 100644 index 0000000000..e6fe30d61a --- /dev/null +++ b/DigitalLearningSolutions.Web/Styles/trackingSystem/contractDetails.scss @@ -0,0 +1,25 @@ +@import "~nhsuk-frontend/packages/core/all"; + +// The colours here are tied to the colour conversion from percentages +// defined in DisplayColourHelper +.coloured-card-number { + &--blue { + color: $color_nhsuk-blue; + } + + &--green { + color: $color_nhsuk-green; + } + + &--yellow { + color: #d97f00; + } + + &--red { + color: $color_nhsuk-red; + } + + &--grey { + color: $color_nhsuk-grey-1; + } +} diff --git a/DigitalLearningSolutions.Web/ViewModels/Common/NumberOfAdministratorsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/Common/NumberOfAdministratorsViewModel.cs index 20c92433fb..c6ffdab939 100644 --- a/DigitalLearningSolutions.Web/ViewModels/Common/NumberOfAdministratorsViewModel.cs +++ b/DigitalLearningSolutions.Web/ViewModels/Common/NumberOfAdministratorsViewModel.cs @@ -4,16 +4,10 @@ using System.Linq; using DigitalLearningSolutions.Data.Models; using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Helpers; public class NumberOfAdministratorsViewModel { - public string Admins { get; set; } - public string Supervisors { get; set; } - public string Trainers { get; set; } - public string CmsAdministrators { get; set; } - public string CmsManagers { get; set; } - public string CcLicences { get; set; } - public NumberOfAdministratorsViewModel(Centre centreDetails, List adminUsers) { Admins = adminUsers.Count(a => a.IsCentreAdmin).ToString(); @@ -24,15 +18,26 @@ public NumberOfAdministratorsViewModel(Centre centreDetails, List adm var cmsManagers = adminUsers.Count(a => a.IsContentManager) - cmsAdministrators; var ccLicences = adminUsers.Count(a => a.IsContentCreator); - Trainers = GenerateDisplayString(trainers, centreDetails.TrainerSpots); - CmsAdministrators = GenerateDisplayString(cmsAdministrators, centreDetails.CmsAdministratorSpots); - CmsManagers = GenerateDisplayString(cmsManagers, centreDetails.CmsManagerSpots); - CcLicences = GenerateDisplayString(ccLicences, centreDetails.CcLicenceSpots); + Trainers = DisplayStringHelper.FormatNumberWithLimit(trainers, centreDetails.TrainerSpots); + CmsAdministrators = DisplayStringHelper.FormatNumberWithLimit( + cmsAdministrators, + centreDetails.CmsAdministratorSpots + ); + CmsManagers = DisplayStringHelper.FormatNumberWithLimit( + cmsManagers, + centreDetails.CmsManagerSpots + ); + CcLicences = DisplayStringHelper.FormatNumberWithLimit( + ccLicences, + centreDetails.CcLicenceSpots + ); } - private string GenerateDisplayString(int number, int limit) - { - return limit == -1 ? number.ToString() : number + " / " + limit; - } + public string Admins { get; set; } + public string Supervisors { get; set; } + public string Trainers { get; set; } + public string CmsAdministrators { get; set; } + public string CmsManagers { get; set; } + public string CcLicences { get; set; } } } diff --git a/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/ContractDetails/ContractDetailsViewModel.cs b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/ContractDetails/ContractDetailsViewModel.cs new file mode 100644 index 0000000000..5ab8121619 --- /dev/null +++ b/DigitalLearningSolutions.Web/ViewModels/TrackingSystem/Centre/ContractDetails/ContractDetailsViewModel.cs @@ -0,0 +1,89 @@ +namespace DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.ContractDetails +{ + using System.Collections.Generic; + using System.Linq; + using DigitalLearningSolutions.Data.Models; + using DigitalLearningSolutions.Data.Models.User; + using DigitalLearningSolutions.Web.Helpers; + + public class ContractDetailsViewModel + { + public ContractDetailsViewModel(List adminUsers, Centre centreDetails, int numberOfCourses) + { + Administrators = adminUsers.Count(a => a.IsCentreAdmin).ToString(); + Supervisors = adminUsers.Count(a => a.IsSupervisor).ToString(); + + var trainers = adminUsers.Count(a => a.IsTrainer); + var cmsAdministrators = adminUsers.Count(a => a.ImportOnly); + var cmsManagers = adminUsers.Count(a => a.IsContentManager) - cmsAdministrators; + var contentCreators = adminUsers.Count(a => a.IsContentCreator); + + Trainers = DisplayStringHelper.FormatNumberWithLimit(trainers, centreDetails.TrainerSpots); + TrainersColour = DisplayColourHelper.GetDisplayColourForPercentage(trainers, centreDetails.TrainerSpots); + + CmsAdministrators = DisplayStringHelper.FormatNumberWithLimit( + cmsAdministrators, + centreDetails.CmsAdministratorSpots + ); + CmsAdministratorsColour = DisplayColourHelper.GetDisplayColourForPercentage( + cmsAdministrators, + centreDetails.CmsAdministratorSpots + ); + + CmsManagers = DisplayStringHelper.FormatNumberWithLimit( + cmsManagers, + centreDetails.CmsManagerSpots + ); + CmsManagersColour = DisplayColourHelper.GetDisplayColourForPercentage( + cmsManagers, + centreDetails.CmsManagerSpots + ); + + ContentCreators = DisplayStringHelper.FormatNumberWithLimit( + contentCreators, + centreDetails.CcLicenceSpots + ); + ContentCreatorsColour = DisplayColourHelper.GetDisplayColourForPercentage( + contentCreators, + centreDetails.CcLicenceSpots + ); + + CustomCourses = DisplayStringHelper.FormatNumberWithLimit( + numberOfCourses, + centreDetails.CustomCourses + ); + CustomCoursesColour = DisplayColourHelper.GetDisplayColourForPercentage(numberOfCourses, centreDetails.CustomCourses); + + ServerSpace = DisplayStringHelper.FormatBytesWithLimit( + centreDetails.ServerSpaceUsed, + centreDetails.ServerSpaceBytes + ); + ServerSpaceColour = DisplayColourHelper.GetDisplayColourForPercentage( + centreDetails.ServerSpaceUsed, + centreDetails.ServerSpaceBytes + ); + } + + public string Administrators { get; set; } + + public string CmsAdministrators { get; set; } + public string CmsAdministratorsColour { get; set; } + + public string CmsManagers { get; set; } + public string CmsManagersColour { get; set; } + + public string ContentCreators { get; set; } + public string ContentCreatorsColour { get; set; } + + public string Trainers { get; set; } + public string TrainersColour { get; set; } + + public string Supervisors { get; set; } + + public string CustomCourses { get; set; } + public string CustomCoursesColour { get; set; } + + public string ServerSpace { get; set; } + public string ServerSpaceColour { get; set; } + } +} diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/ContractDetails/Index.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/ContractDetails/Index.cshtml new file mode 100644 index 0000000000..b7b6cc125c --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/ContractDetails/Index.cshtml @@ -0,0 +1,41 @@ +@inject IConfiguration Configuration + +@using DigitalLearningSolutions.Web.Helpers +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.ContractDetails +@using Microsoft.Extensions.Configuration +@model ContractDetailsViewModel + + + +@{ + ViewData["Title"] = "Contract details"; + ViewData["Application"] = "Tracking System"; + ViewData["HeaderPath"] = $"{Configuration["AppRootPath"]}/TrackingSystem/Centre/Dashboard"; + ViewData["HeaderPathName"] = "Tracking System"; +} + +@section NavMenuItems { + +} + +@section NavBreadcrumbs { + +} + +
+
+

Contract details

+ + + + + View pricing plans + + @if (User.HasCentreManagerPermissions()) + { + + Manage users + + } +
+
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/ContractDetails/_ContractDetailsList.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/ContractDetails/_ContractDetailsList.cshtml new file mode 100644 index 0000000000..b60e0b558d --- /dev/null +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/ContractDetails/_ContractDetailsList.cshtml @@ -0,0 +1,92 @@ +@using DigitalLearningSolutions.Web.ViewModels.TrackingSystem.Centre.ContractDetails +@model ContractDetailsViewModel + +
    +
  • +
    +
    +

    + @Model.Administrators +

    + Administrators +
    +
    +
  • + +
  • +
    +
    +

    + @Model.CmsAdministrators +

    + CMS administrators +
    +
    +
  • + +
  • +
    +
    +

    + @Model.CmsManagers +

    + CMS managers +
    +
    +
  • + +
  • +
    +
    +

    + @Model.Trainers +

    + Trainers +
    +
    +
  • + +
  • +
    +
    +

    + @Model.Supervisors +

    + Supervisors +
    +
    +
  • + +
  • +
    +
    +

    + @Model.ContentCreators +

    + Content creators +
    +
    +
  • + +
  • +
    +
    +

    + @Model.CustomCourses +

    + Custom courses +
    +
    +
  • + +
  • +
    +
    +

    + @Model.ServerSpace +

    + Server space +
    +
    +
  • +
diff --git a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/Dashboard/_DashboardBottomCardGroup.cshtml b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/Dashboard/_DashboardBottomCardGroup.cshtml index 5471d18df9..6afc369f0b 100644 --- a/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/Dashboard/_DashboardBottomCardGroup.cshtml +++ b/DigitalLearningSolutions.Web/Views/TrackingSystem/Centre/Dashboard/_DashboardBottomCardGroup.cshtml @@ -4,7 +4,7 @@

- Contract details + Contract details

See details about your contract type