From cc4781b990ce096c69b5379d895a4eb956ca0406 Mon Sep 17 00:00:00 2001 From: Chase Tucker Date: Wed, 19 Mar 2025 10:01:35 -0700 Subject: [PATCH] PCRB follow up client side logic --- .../ApprovalServiceTests.cs | 55 ++ .../MockMemoryCacheService.cs | 18 + .../PCRBFollowUpCommentsTests.cs | 196 +++++++ MesaFabApproval.API.Test/PCRBFollowUpTests.cs | 198 +++++++ MesaFabApproval.API.Test/PCRBServiceTests.cs | 312 ++++------ .../Controllers/ApprovalController.cs | 171 +++--- .../Controllers/PCRBController.cs | 170 +++++- .../Services/ApprovalService.cs | 41 +- .../Services/AuthenticationService.cs | 2 +- MesaFabApproval.API/Services/PCRBService.cs | 209 ++++++- .../ApprovalServiceTests.cs | 95 ++++ .../MockMemoryCacheService.cs | 18 + .../PCRBFollowUpCommentTests.cs | 320 +++++++++++ .../PCRBFollowUpTests.cs | 321 +++++++++++ .../PCRBServiceTests.cs | 343 +++-------- .../MesaFabApproval.Client.csproj | 16 +- .../Pages/AuthenticatedRedirect.razor.cs | 12 +- .../Pages/Components/Comments.razor | 2 +- .../MRBActionCommentsAndFiles.razor | 2 +- .../Pages/Components/MRBActionForm.razor | 2 +- .../Components/MRBApproverSelector.razor | 2 +- .../Pages/Components/PCR3DocumentForm.razor | 2 +- .../Pages/Components/PCRBActionItemForm.razor | 2 +- .../Pages/Components/PCRBApproverForm.razor | 2 +- .../Pages/Components/PCRBAttachmentForm.razor | 2 +- .../Pages/Components/UserSelector.razor | 2 +- MesaFabApproval.Client/Pages/Dashboard.razor | 4 +- .../Pages/Dashboard.razor.cs | 10 +- MesaFabApproval.Client/Pages/Login.razor | 65 --- MesaFabApproval.Client/Pages/Login.razor.cs | 53 ++ MesaFabApproval.Client/Pages/MRBAll.razor | 44 -- MesaFabApproval.Client/Pages/MRBAll.razor.cs | 55 ++ .../Pages/MRBSingle.razor.cs | 6 +- MesaFabApproval.Client/Pages/PCRBAll.razor | 90 ++- MesaFabApproval.Client/Pages/PCRBAll.razor.cs | 64 +++ MesaFabApproval.Client/Pages/PCRBSingle.razor | 537 ++++++++++++------ .../Pages/PCRBSingle.razor.cs | 471 +++++++++++++-- .../Services/ApprovalService.cs | 15 + .../MesaFabApprovalAuthStateProvider.cs | 7 +- .../Services/PCRBService.cs | 118 +++- .../Utilities/ApiHttpClientHandler.cs | 2 +- MesaFabApproval.Shared/Models/PCRB.cs | 15 +- MesaFabApproval.Shared/Models/PCRBFollowUp.cs | 3 +- .../Models/PCRBFollowUpComment.cs | 13 + .../Models/PCRBNotification.cs | 3 + 45 files changed, 3082 insertions(+), 1008 deletions(-) create mode 100644 MesaFabApproval.API.Test/ApprovalServiceTests.cs create mode 100644 MesaFabApproval.API.Test/MockMemoryCacheService.cs create mode 100644 MesaFabApproval.API.Test/PCRBFollowUpCommentsTests.cs create mode 100644 MesaFabApproval.API.Test/PCRBFollowUpTests.cs create mode 100644 MesaFabApproval.Client.Test/ApprovalServiceTests.cs create mode 100644 MesaFabApproval.Client.Test/MockMemoryCacheService.cs create mode 100644 MesaFabApproval.Client.Test/PCRBFollowUpCommentTests.cs create mode 100644 MesaFabApproval.Client.Test/PCRBFollowUpTests.cs create mode 100644 MesaFabApproval.Client/Pages/Login.razor.cs create mode 100644 MesaFabApproval.Client/Pages/MRBAll.razor.cs create mode 100644 MesaFabApproval.Client/Pages/PCRBAll.razor.cs create mode 100644 MesaFabApproval.Shared/Models/PCRBFollowUpComment.cs diff --git a/MesaFabApproval.API.Test/ApprovalServiceTests.cs b/MesaFabApproval.API.Test/ApprovalServiceTests.cs new file mode 100644 index 0000000..987087e --- /dev/null +++ b/MesaFabApproval.API.Test/ApprovalServiceTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; + +using MesaFabApproval.API.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +using Moq; + +using Xunit; + +namespace MesaFabApproval.API.Test; + +public class ApprovalServiceTests { + private readonly Mock> _loggerMock; + private readonly Mock _cacheMock; + private readonly Mock _dalServiceMock; + private readonly Mock _userServiceMock; + private readonly ApprovalService _approvalService; + + public ApprovalServiceTests() { + _loggerMock = new Mock>(); + _cacheMock = new Mock(); + _dalServiceMock = new Mock(); + _userServiceMock = new Mock(); + _approvalService = new ApprovalService(_loggerMock.Object, _cacheMock.Object, _dalServiceMock.Object, _userServiceMock.Object); + } + + [Fact] + public async Task DeleteApproval_ValidApprovalID_DeletesApproval() { + int approvalID = 1; + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())).ReturnsAsync(1); + + await _approvalService.DeleteApproval(approvalID); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task DeleteApproval_InvalidApprovalID_ThrowsArgumentException() { + int approvalID = 0; + + await Assert.ThrowsAsync(() => _approvalService.DeleteApproval(approvalID)); + } + + [Fact] + public async Task DeleteApproval_DeletionFails_ThrowsException() { + int approvalID = 1; + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())).ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _approvalService.DeleteApproval(approvalID)); + } +} diff --git a/MesaFabApproval.API.Test/MockMemoryCacheService.cs b/MesaFabApproval.API.Test/MockMemoryCacheService.cs new file mode 100644 index 0000000..89f0482 --- /dev/null +++ b/MesaFabApproval.API.Test/MockMemoryCacheService.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Caching.Memory; + +using Moq; + +namespace MesaFabApproval.API.Test; + +public static class MockMemoryCacheService { + public static Mock GetMemoryCache(object expectedValue) { + Mock mockMemoryCache = new Mock(); + mockMemoryCache + .Setup(x => x.TryGetValue(It.IsAny(), out expectedValue)) + .Returns(true); + mockMemoryCache + .Setup(x => x.CreateEntry(It.IsAny())) + .Returns(Mock.Of()); + return mockMemoryCache; + } +} diff --git a/MesaFabApproval.API.Test/PCRBFollowUpCommentsTests.cs b/MesaFabApproval.API.Test/PCRBFollowUpCommentsTests.cs new file mode 100644 index 0000000..b8383ff --- /dev/null +++ b/MesaFabApproval.API.Test/PCRBFollowUpCommentsTests.cs @@ -0,0 +1,196 @@ +using MesaFabApproval.API.Services; +using MesaFabApproval.Models; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +using Moq; + +namespace MesaFabApproval.API.Test; + +public class PCRBFollowUpCommentsTests { + private readonly Mock> _loggerMock; + private readonly Mock _dalServiceMock; + private readonly Mock _cacheMock; + private readonly Mock _userServiceMock; + private readonly Mock _approvalServiceMock; + private readonly Mock _smtpServiceMock; + private readonly PCRBService _pcrbService; + private readonly AppSettings _appSettings; + + private static PCRBFollowUpComment FOLLOW_UP_COMMENT = new PCRBFollowUpComment { + PlanNumber = 1, + FollowUpID = 1, + Comment = "Comment", + UserID = 1 + }; + + private static IEnumerable FOLLOW_UP_COMMENTS = new List() { FOLLOW_UP_COMMENT }; + + public PCRBFollowUpCommentsTests() { + _loggerMock = new Mock>(); + _dalServiceMock = new Mock(); + _userServiceMock = new Mock(); + _approvalServiceMock = new Mock(); + _smtpServiceMock = new Mock(); + _cacheMock = MockMemoryCacheService.GetMemoryCache(FOLLOW_UP_COMMENTS); + _appSettings = new AppSettings( + Company: "Infineon", + DbConnectionString: "connectionString", + JwtAudience: "audience", + JwtIssuer: "issuer", + JwtKey: "key", + MrbAttachmentPath: "mrbAttachmentPath", + PcrbAttachmentPath: "pcrbAttachmentPath", + ShouldSendEmail: false, + SiteBaseUrl: "siteBaseUrl", + WorkingDirectoryName: "workingDirectoryName" + ); + + _pcrbService = new PCRBService( + _loggerMock.Object, + _dalServiceMock.Object, + _cacheMock.Object, + _userServiceMock.Object, + _approvalServiceMock.Object, + _smtpServiceMock.Object, + _appSettings + ); + } + + [Fact] + public async Task CreateFollowUpComment_WithValidParam_ShouldCreateFollowUp() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT)) + .ReturnsAsync(1); + + await _pcrbService.CreateFollowUpComment(FOLLOW_UP_COMMENT); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT), Times.Once); + } + + [Fact] + public async Task CreateFollowUpComment_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUpComment(null)); + } + + [Fact] + public async Task CreateFollowUpComment_WithDatabaseFailure_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT)) + .ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUpComment(FOLLOW_UP_COMMENT)); + } + + [Fact] + public async Task CreateFollowUpComment_WithDatabaseException_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT)) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUpComment(FOLLOW_UP_COMMENT)); + } + + [Fact] + public async Task GetFollowUpCommentsByPlanNumber_WithCacheBypass_ShouldReturnFollowUps() { + int planNumber = 1; + + _dalServiceMock.Setup(d => d.QueryAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(FOLLOW_UP_COMMENTS); + + IEnumerable result = await _pcrbService.GetFollowUpCommentsByPlanNumber(planNumber, true); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(FOLLOW_UP_COMMENTS, result); + _dalServiceMock.Verify(d => d.QueryAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task GetFollowUpCommentsByPlanNumber_WithCacheBypass_AndDatabaseException_ShouldThrowException() { + int planNumber = 1; + + _dalServiceMock.Setup(d => d.QueryAsync(It.IsAny(), It.IsAny())) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpCommentsByPlanNumber(planNumber, true)); + } + + [Fact] + public async Task GetFollowUpCommentsByPlanNumber_WithoutCacheBypass_ShouldReturnFollowUps() { + int planNumber = 1; + + IEnumerable result = await _pcrbService.GetFollowUpCommentsByPlanNumber(planNumber, false); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(FOLLOW_UP_COMMENTS, result); + } + + [Fact] + public async Task UpdateFollowUpComment_WithValidParam_ShouldUpdateFollowUp() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT)) + .ReturnsAsync(1); + + await _pcrbService.UpdateFollowUpComment(FOLLOW_UP_COMMENT); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT), Times.Once); + } + + [Fact] + public async Task UpdateFollowUpComment_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUpComment(null)); + } + + [Fact] + public async Task UpdateFollowUpComment_WithDatabaseFailure_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT)) + .ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUpComment(FOLLOW_UP_COMMENT)); + } + + [Fact] + public async Task UpdateFollowUpComment_WithDatabaseException_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP_COMMENT)) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUpComment(FOLLOW_UP_COMMENT)); + } + + [Fact] + public async Task DeleteFollowUpComment_WithValidId_ShouldDeleteFollowUp() { + int commentId = 1; + + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(1); + + await _pcrbService.DeleteFollowUpComment(commentId); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task DeleteFollowUpComment_WithInvalidId_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUpComment(0)); + } + + [Fact] + public async Task DeleteFollowUpComment_WithDatabaseFailure_ShouldThrowException() { + int commentId = 1; + + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUpComment(commentId)); + } + + [Fact] + public async Task DeleteFollowUpComment_WithDatabaseException_ShouldThrowException() { + int commentId = 1; + + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUpComment(commentId)); + } +} diff --git a/MesaFabApproval.API.Test/PCRBFollowUpTests.cs b/MesaFabApproval.API.Test/PCRBFollowUpTests.cs new file mode 100644 index 0000000..dd6643c --- /dev/null +++ b/MesaFabApproval.API.Test/PCRBFollowUpTests.cs @@ -0,0 +1,198 @@ +using MesaFabApproval.API.Services; +using MesaFabApproval.Models; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging; + +using Moq; + +namespace MesaFabApproval.API.Test; + +public class PCRBFollowUpTests { + private readonly Mock> _loggerMock; + private readonly Mock _dalServiceMock; + private readonly Mock _cacheMock; + private readonly Mock _userServiceMock; + private readonly Mock _approvalServiceMock; + private readonly Mock _smtpServiceMock; + private readonly PCRBService _pcrbService; + private readonly AppSettings _appSettings; + + private static PCRBFollowUp FOLLOW_UP = new PCRBFollowUp { + ID = 1, + PlanNumber = 1, + Step = 1, + FollowUpDate = DateTime.Now + }; + + private static IEnumerable FOLLOW_UPS = new List() { + new PCRBFollowUp { ID = 1, PlanNumber = 1, Step = 1, FollowUpDate = DateTime.Now } + }; + + public PCRBFollowUpTests() { + _loggerMock = new Mock>(); + _dalServiceMock = new Mock(); + _userServiceMock = new Mock(); + _approvalServiceMock = new Mock(); + _smtpServiceMock = new Mock(); + _cacheMock = MockMemoryCacheService.GetMemoryCache(FOLLOW_UPS); + _appSettings = new AppSettings( + Company: "Infineon", + DbConnectionString: "connectionString", + JwtAudience: "audience", + JwtIssuer: "issuer", + JwtKey: "key", + MrbAttachmentPath: "mrbAttachmentPath", + PcrbAttachmentPath: "pcrbAttachmentPath", + ShouldSendEmail: false, + SiteBaseUrl: "siteBaseUrl", + WorkingDirectoryName: "workingDirectoryName" + ); + + _pcrbService = new PCRBService( + _loggerMock.Object, + _dalServiceMock.Object, + _cacheMock.Object, + _userServiceMock.Object, + _approvalServiceMock.Object, + _smtpServiceMock.Object, + _appSettings + ); + } + + [Fact] + public async Task CreateFollowUp_WithValidParam_ShouldCreateFollowUp() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP)) + .ReturnsAsync(1); + + await _pcrbService.CreateFollowUp(FOLLOW_UP); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP), Times.Once); + } + + [Fact] + public async Task CreateFollowUp_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(null)); + } + + [Fact] + public async Task CreateFollowUp_WithDatabaseFailure_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP)) + .ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(FOLLOW_UP)); + } + + [Fact] + public async Task CreateFollowUp_WithDatabaseException_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP)) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(FOLLOW_UP)); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithCacheBypass_ShouldReturnFollowUps() { + int planNumber = 1; + + _dalServiceMock.Setup(d => d.QueryAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(FOLLOW_UPS); + + IEnumerable result = await _pcrbService.GetFollowUpsByPlanNumber(planNumber, true); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(FOLLOW_UPS, result); + _dalServiceMock.Verify(d => d.QueryAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithCacheBypass_AndDatabaseException_ShouldThrowException() { + int planNumber = 1; + + _dalServiceMock.Setup(d => d.QueryAsync(It.IsAny(), It.IsAny())) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpsByPlanNumber(planNumber, true)); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithoutCacheBypass_ShouldReturnFollowUps() { + int planNumber = 1; + + IEnumerable result = await _pcrbService.GetFollowUpsByPlanNumber(planNumber, false); + + Assert.NotNull(result); + Assert.Single(result); + Assert.Equal(FOLLOW_UPS, result); + } + + [Fact] + public async Task UpdateFollowUp_WithValidParam_ShouldUpdateFollowUp() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP)) + .ReturnsAsync(1); + + await _pcrbService.UpdateFollowUp(FOLLOW_UP); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP), Times.Once); + } + + [Fact] + public async Task UpdateFollowUp_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(null)); + } + + [Fact] + public async Task UpdateFollowUp_WithDatabaseFailure_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP)) + .ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(FOLLOW_UP)); + } + + [Fact] + public async Task UpdateFollowUp_WithDatabaseException_ShouldThrowException() { + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), FOLLOW_UP)) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(FOLLOW_UP)); + } + + [Fact] + public async Task DeleteFollowUp_WithValidId_ShouldDeleteFollowUp() { + int followUpId = 1; + + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(1); + + await _pcrbService.DeleteFollowUp(followUpId); + + _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task DeleteFollowUp_WithInvalidId_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(0)); + } + + [Fact] + public async Task DeleteFollowUp_WithDatabaseFailure_ShouldThrowException() { + int followUpId = 1; + + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(followUpId)); + } + + [Fact] + public async Task DeleteFollowUp_WithDatabaseException_ShouldThrowException() { + int followUpId = 1; + + _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) + .Throws(); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(followUpId)); + } +} diff --git a/MesaFabApproval.API.Test/PCRBServiceTests.cs b/MesaFabApproval.API.Test/PCRBServiceTests.cs index c3a2e7d..d33e86d 100644 --- a/MesaFabApproval.API.Test/PCRBServiceTests.cs +++ b/MesaFabApproval.API.Test/PCRBServiceTests.cs @@ -1,28 +1,15 @@ -using MesaFabApproval.API.Services; +using MesaFabApproval.API.Services; using MesaFabApproval.Models; using MesaFabApproval.Shared.Models; - using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; - using Moq; +using System.Net.Mail; + namespace MesaFabApproval.API.Test; - -public static class MockMemoryCacheService { - public static Mock GetMemoryCache(object expectedValue) { - Mock mockMemoryCache = new Mock(); - mockMemoryCache - .Setup(x => x.TryGetValue(It.IsAny(), out expectedValue)) - .Returns(true); - mockMemoryCache - .Setup(x => x.CreateEntry(It.IsAny())) - .Returns(Mock.Of()); - return mockMemoryCache; - } -} - -public class PCRBServiceTests { +public class PCRBServiceTests +{ private readonly Mock> _loggerMock; private readonly Mock _dalServiceMock; private Mock _cacheMock; @@ -47,24 +34,25 @@ public class PCRBServiceTests { } }; - private static IEnumerable FOLLOW_UPS = new List() { + private static IEnumerable FOLLOW_UPS = new List() { new PCRBFollowUp { ID = 1, PlanNumber = 1, Step = 1, FollowUpDate = DateTime.Now } }; private static AppSettings appSettings = new AppSettings( - Company: "Infineon", - DbConnectionString: "connectionString", - JwtAudience: "audience", - JwtIssuer: "issuer", - JwtKey: "key", - MrbAttachmentPath: "mrbAttachmentPath", - PcrbAttachmentPath: "pcrbAttachmentPath", - ShouldSendEmail: false, - SiteBaseUrl: "siteBaseUrl", - WorkingDirectoryName: "workingDirectoryName" - ); + Company: "Infineon", + DbConnectionString: "connectionString", + JwtAudience: "audience", + JwtIssuer: "issuer", + JwtKey: "key", + MrbAttachmentPath: "mrbAttachmentPath", + PcrbAttachmentPath: "pcrbAttachmentPath", + ShouldSendEmail: false, + SiteBaseUrl: "siteBaseUrl", + WorkingDirectoryName: "workingDirectoryName" + ); - public PCRBServiceTests() { + public PCRBServiceTests() + { _loggerMock = new Mock>(); _dalServiceMock = new Mock(); _userServiceMock = new Mock(); @@ -84,8 +72,10 @@ public class PCRBServiceTests { } [Fact] - public async Task CreateNewPCRB_WithValidParam_ShouldCreatePCRB() { - var pcrb = new PCRB { + public async Task CreateNewPCRB_WithValidParam_ShouldCreatePCRB() + { + var pcrb = new PCRB + { OwnerID = 1, Title = "Test Title", ChangeLevel = "Level 1", @@ -107,13 +97,16 @@ public class PCRBServiceTests { } [Fact] - public async Task CreateNewPCRB_WithNullParam_ShouldThrowException() { + public async Task CreateNewPCRB_WithNullParam_ShouldThrowException() + { await Assert.ThrowsAsync(() => _pcrbService.CreateNewPCRB(null)); } [Fact] - public async Task CreateNewPCRB_WithDatabaseFailure_ShouldThrowException() { - var pcrb = new PCRB { + public async Task CreateNewPCRB_WithDatabaseFailure_ShouldThrowException() + { + var pcrb = new PCRB + { OwnerID = 1, Title = "Test Title", ChangeLevel = "Level 1", @@ -133,8 +126,10 @@ public class PCRBServiceTests { } [Fact] - public async Task CreateNewPCRB_WithDatabaseException_ShouldThrowException() { - var pcrb = new PCRB { + public async Task CreateNewPCRB_WithDatabaseException_ShouldThrowException() + { + var pcrb = new PCRB + { OwnerID = 1, Title = "Test Title", ChangeLevel = "Level 1", @@ -154,7 +149,8 @@ public class PCRBServiceTests { } [Fact] - public async Task UpdatePCRB_WithValidParam_ShouldUpdatePCRB() { + public async Task UpdatePCRB_WithValidParam_ShouldUpdatePCRB() + { _cacheMock = MockMemoryCacheService.GetMemoryCache(PCRBS); _pcrbService = new PCRBService( @@ -167,7 +163,8 @@ public class PCRBServiceTests { appSettings ); - var pcrb = new PCRB { + var pcrb = new PCRB + { PlanNumber = 1, OwnerID = 1, Title = "Test Title", @@ -190,13 +187,16 @@ public class PCRBServiceTests { } [Fact] - public async Task UpdatePCRB_WithNullParam_ShouldThrowException() { + public async Task UpdatePCRB_WithNullParam_ShouldThrowException() + { await Assert.ThrowsAsync(() => _pcrbService.UpdatePCRB(null)); } [Fact] - public async Task UpdatePCRB_WithDatabaseFailure_ShouldThrowException() { - var pcrb = new PCRB { + public async Task UpdatePCRB_WithDatabaseFailure_ShouldThrowException() + { + var pcrb = new PCRB + { PlanNumber = 1, OwnerID = 1, Title = "Test Title", @@ -217,8 +217,10 @@ public class PCRBServiceTests { } [Fact] - public async Task UpdatePCRB_WithDatabaseException_ShouldThrowException() { - var pcrb = new PCRB { + public async Task UpdatePCRB_WithDatabaseException_ShouldThrowException() + { + var pcrb = new PCRB + { PlanNumber = 1, OwnerID = 1, Title = "Test Title", @@ -239,176 +241,88 @@ public class PCRBServiceTests { } [Fact] - public async Task CreateFollowUp_WithValidParam_ShouldCreateFollowUp() { - var followUp = new PCRBFollowUp { - PlanNumber = 1, - Step = 1, - FollowUpDate = DateTime.Now + public async Task NotifyApprover_ShouldSendNotification() + { + PCRBNotification notification = new PCRBNotification + { + Message = "Test Message", + PCRB = new PCRB { PlanNumber = 1, Title = "Test PCRB" }, + Approval = new Approval + { + UserID = 1, + IssueID = 1, + RoleName = "Role", + SubRole = "SubRole", + SubRoleID = 1, + AssignedDate = DateTime.Now + } }; - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), followUp)) - .ReturnsAsync(1); + _userServiceMock.Setup(s => s.GetUserByUserId(It.IsAny())) + .ReturnsAsync(new User + { + UserID = 1, + LoginID = "testLogin", + FirstName = "Test", + LastName = "User", + Email = "test@example.com" + }); - await _pcrbService.CreateFollowUp(followUp); + _smtpServiceMock.Setup(s => s.SendEmail(It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(true); - _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), followUp), Times.Once); + _approvalServiceMock.Setup(s => s.UpdateApproval(It.IsAny())) + .Returns(Task.CompletedTask); + + await _pcrbService.NotifyApprover(notification); + + _smtpServiceMock.Verify(s => s.SendEmail(It.IsAny>(), + It.IsAny>(), + It.IsAny(), + It.IsAny()), Times.Once); + _approvalServiceMock.Verify(s => s.UpdateApproval(It.IsAny()), Times.Once); } [Fact] - public async Task CreateFollowUp_WithNullParam_ShouldThrowException() { - await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(null)); + public async Task NotifyApprover_ShouldThrowException_WhenNotificationIsNull() + { + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(null)); } [Fact] - public async Task CreateFollowUp_WithDatabaseFailure_ShouldThrowException() { - var followUp = new PCRBFollowUp { - PlanNumber = 1, - Step = 1, - FollowUpDate = DateTime.Now + public async Task NotifyApprover_ShouldThrowException_WhenPCRBIsNull() + { + PCRBNotification notification = new PCRBNotification + { + Message = "Test Message", + PCRB = null, + Approval = new Approval + { + UserID = 1, + IssueID = 1, + RoleName = "Role", + SubRole = "SubRole", + SubRoleID = 1, + AssignedDate = DateTime.Now + } }; - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), followUp)) - .ReturnsAsync(0); - - await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(followUp)); + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); } [Fact] - public async Task CreateFollowUp_WithDatabaseException_ShouldThrowException() { - var followUp = new PCRBFollowUp { - PlanNumber = 1, - Step = 1, - FollowUpDate = DateTime.Now + public async Task NotifyApprover_ShouldThrowException_WhenApprovalIsNull() + { + PCRBNotification notification = new PCRBNotification + { + Message = "Test Message", + PCRB = new PCRB { PlanNumber = 1, Title = "Test PCRB" }, + Approval = null }; - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), followUp)) - .Throws(); - - await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(followUp)); - } - - [Fact] - public async Task GetFollowUpsByPlanNumber_WithCacheBypass_ShouldReturnFollowUps() { - int planNumber = 1; - - _dalServiceMock.Setup(d => d.QueryAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(FOLLOW_UPS); - - IEnumerable result = await _pcrbService.GetFollowUpsByPlanNumber(planNumber, true); - - Assert.NotNull(result); - Assert.Single(result); - Assert.Equal(FOLLOW_UPS, result); - _dalServiceMock.Verify(d => d.QueryAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task GetFollowUpsByPlanNumber_WithCacheBypass_AndDatabaseException_ShouldThrowException() { - int planNumber = 1; - - _dalServiceMock.Setup(d => d.QueryAsync(It.IsAny(), It.IsAny())) - .Throws(); - - await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpsByPlanNumber(planNumber, true)); - } - - [Fact] - public async Task GetFollowUpsByPlanNumber_WithoutCacheBypass_ShouldReturnFollowUps() { - int planNumber = 1; - - IEnumerable result = await _pcrbService.GetFollowUpsByPlanNumber(planNumber, false); - - Assert.NotNull(result); - Assert.Single(result); - Assert.Equal(FOLLOW_UPS, result); - } - - [Fact] - public async Task UpdateFollowUp_WithValidParam_ShouldUpdateFollowUp() { - var followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 1, - Step = 1, - FollowUpDate = DateTime.Now - }; - - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), followUp)) - .ReturnsAsync(1); - - await _pcrbService.UpdateFollowUp(followUp); - - _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), followUp), Times.Once); - } - - [Fact] - public async Task UpdateFollowUp_WithNullParam_ShouldThrowException() { - await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(null)); - } - - [Fact] - public async Task UpdateFollowUp_WithDatabaseFailure_ShouldThrowException() { - var followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 1, - Step = 1, - FollowUpDate = DateTime.Now - }; - - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), followUp)) - .ReturnsAsync(0); - - await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(followUp)); - } - - [Fact] - public async Task UpdateFollowUp_WithDatabaseException_ShouldThrowException() { - var followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 1, - Step = 1, - FollowUpDate = DateTime.Now - }; - - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), followUp)) - .Throws(); - - await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(followUp)); - } - - [Fact] - public async Task DeleteFollowUp_WithValidId_ShouldDeleteFollowUp() { - int followUpId = 1; - - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(1); - - await _pcrbService.DeleteFollowUp(followUpId); - - _dalServiceMock.Verify(d => d.ExecuteAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public async Task DeleteFollowUp_WithInvalidId_ShouldThrowException() { - await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(0)); - } - - [Fact] - public async Task DeleteFollowUp_WithDatabaseFailure_ShouldThrowException() { - int followUpId = 1; - - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(0); - - await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(followUpId)); - } - - [Fact] - public async Task DeleteFollowUp_WithDatabaseException_ShouldThrowException() { - int followUpId = 1; - - _dalServiceMock.Setup(d => d.ExecuteAsync(It.IsAny(), It.IsAny())) - .Throws(); - - await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(followUpId)); + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); } } diff --git a/MesaFabApproval.API/Controllers/ApprovalController.cs b/MesaFabApproval.API/Controllers/ApprovalController.cs index aaa6257..d679f0f 100644 --- a/MesaFabApproval.API/Controllers/ApprovalController.cs +++ b/MesaFabApproval.API/Controllers/ApprovalController.cs @@ -1,4 +1,5 @@ using MesaFabApproval.API.Services; +using MesaFabApproval.API.Utilities; using MesaFabApproval.Shared.Models; using MesaFabApproval.Shared.Services; @@ -11,13 +12,13 @@ namespace MesaFabApproval.API.Controllers; public class ApprovalController : ControllerBase { private readonly ILogger _logger; private readonly IApprovalService _approvalService; - private readonly IMonInWorkerClient _monInClient; + private readonly IMonInUtils _monInUtils; public ApprovalController(ILogger logger, IApprovalService approvalService, - IMonInWorkerClient monInClient) { + IMonInUtils monInUtils) { _logger = logger ?? throw new ArgumentNullException("ILogger not injected"); _approvalService = approvalService ?? throw new ArgumentNullException("IApprovalService not injected"); - _monInClient = monInClient ?? throw new ArgumentNullException("IMonInWorkerClient not injected"); + _monInUtils = monInUtils ?? throw new ArgumentNullException("IMonInUtils not injected"); } [HttpPost] @@ -39,26 +40,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to create approval: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot create new approval, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "CreateApproval"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -81,26 +75,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when getting approvals for issue {issueId}: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot get approvals, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "GetApprovalsForIssueId"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -123,26 +110,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when getting approvals for user {userId}: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot get approvals, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "GetApprovalsForUserId"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -165,26 +145,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when getting approval group members for sub role {subRoleId}: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot get approval group members, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "GetApprovalsGroupMembers"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -197,7 +170,7 @@ public class ApprovalController : ControllerBase { string errorMessage = ""; try { - _logger.LogInformation($"Attempting to update approval"); + _logger.LogInformation("Attempting to update approval"); if (approval is null) throw new ArgumentNullException($"approval cannot be null"); @@ -207,26 +180,54 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to update approval: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot update approval, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "UpdateApproval"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); + } + } + + [HttpDelete] + [Route("approval")] + public async Task DeleteApproval(int approvalID) { + DateTime start = DateTime.Now; + bool isArgumentError = false; + bool isInternalError = false; + string errorMessage = ""; + + try { + _logger.LogInformation($"Attempting to delete approval {approvalID}"); + + if (approvalID <= 0) throw new ArgumentException("Invalid approval ID"); + + await _approvalService.DeleteApproval(approvalID); + + return Ok(); + } catch (ArgumentException ex) { + isArgumentError = true; + errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to delete approval: {errorMessage}"); + return BadRequest(errorMessage); + } catch (Exception ex) { + isInternalError = true; + errorMessage = $"Cannot delete approval, because {ex.Message}"; + _logger.LogError(errorMessage); + return Problem(errorMessage); + } finally { + string metricName = "UpdateApproval"; + DateTime end = DateTime.Now; + double millisecondsDiff = (end - start).TotalMilliseconds; + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -249,26 +250,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to approve: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot approve, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "Approve"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -291,26 +285,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to deny approval: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Approval denial failed, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "Deny"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -333,26 +320,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to get role ID by role name: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot get role ID, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "GetRoleIdForRoleName"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } @@ -376,26 +356,19 @@ public class ApprovalController : ControllerBase { } catch (ArgumentException ex) { isArgumentError = true; errorMessage = ex.Message; + _logger.LogWarning($"Argument error when attempting to get sub roles by sub role name: {errorMessage}"); return BadRequest(errorMessage); } catch (Exception ex) { isInternalError = true; errorMessage = $"Cannot get role ID, because {ex.Message}"; + _logger.LogError(errorMessage); return Problem(errorMessage); } finally { string metricName = "GetSubRoleIdForSubRoleName"; DateTime end = DateTime.Now; double millisecondsDiff = (end - start).TotalMilliseconds; - _monInClient.PostAverage(metricName + "Latency", millisecondsDiff); - - if (isArgumentError) { - _logger.LogWarning(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Ok); - } else if (isInternalError) { - _logger.LogError(errorMessage); - _monInClient.PostStatus(metricName, StatusValue.Critical); - } else { - _monInClient.PostStatus(metricName, StatusValue.Ok); - } + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } } \ No newline at end of file diff --git a/MesaFabApproval.API/Controllers/PCRBController.cs b/MesaFabApproval.API/Controllers/PCRBController.cs index 7d125a6..1aae281 100644 --- a/MesaFabApproval.API/Controllers/PCRBController.cs +++ b/MesaFabApproval.API/Controllers/PCRBController.cs @@ -790,6 +790,42 @@ public class PCRBController : ControllerBase { } } + [HttpPost] + [Route("pcrb/notify/approver")] + public async Task NotifyApprover(PCRBNotification notification) { + DateTime start = DateTime.Now; + bool isArgumentError = false; + bool isInternalError = false; + string errorMessage = ""; + + try { + _logger.LogInformation("Attempting to notify an approver"); + + if (notification is null) throw new ArgumentNullException("notification cannot be null"); + if (notification.PCRB is null) throw new ArgumentNullException("PCRB cannot be null"); + if (notification.Approval is null) throw new ArgumentNullException("approval cannot be null"); + if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty"); + + await _pcrbService.NotifyApprover(notification); + + return Ok(); + } catch (ArgumentException ex) { + isArgumentError = true; + errorMessage = ex.Message; + return BadRequest(errorMessage); + } catch (Exception ex) { + isInternalError = true; + errorMessage = $"Unable to notify an approver, because {ex.Message}"; + return Problem(errorMessage); + } finally { + string metricName = "NotifyPCRBApprover"; + DateTime end = DateTime.Now; + double millisecondsDiff = (end - start).TotalMilliseconds; + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); + } + } + [HttpPost] [Route("pcrb/notify/approvers")] public async Task NotifyApprovers(PCRBNotification notification) { @@ -937,7 +973,7 @@ public class PCRBController : ControllerBase { string errorMessage = ""; try { - _logger.LogInformation($"Attempting to get attendees for plan# {planNumber}"); + _logger.LogInformation($"Attempting to get follow ups for plan# {planNumber}"); if (planNumber <= 0) throw new ArgumentException($"{planNumber} is not a valid PCRB Plan#"); @@ -1026,4 +1062,136 @@ public class PCRBController : ControllerBase { _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); } } + + [HttpPost] + [Route("pcrb/followUpComment")] + public async Task CreateFollowUpComment(PCRBFollowUpComment comment) { + DateTime start = DateTime.Now; + bool isArgumentError = false; + bool isInternalError = false; + string errorMessage = ""; + + try { + _logger.LogInformation("Attempting to create follow up comment"); + + if (comment is null) throw new ArgumentNullException("comment cannot be null"); + + await _pcrbService.CreateFollowUpComment(comment); + + return Ok(); + } catch (ArgumentException ex) { + isArgumentError = true; + errorMessage = ex.Message; + return BadRequest(errorMessage); + } catch (Exception ex) { + isInternalError = true; + errorMessage = $"Unable to create follow up comment, because {ex.Message}"; + return Problem(errorMessage); + } finally { + string metricName = "CreatePCRBFollowUpComment"; + DateTime end = DateTime.Now; + double millisecondsDiff = (end - start).TotalMilliseconds; + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); + } + } + + [HttpGet] + [Route("pcrb/followUpComments")] + public async Task GetFollowUpCommentsByPlanNumber(int planNumber, bool bypassCache) { + DateTime start = DateTime.Now; + bool isArgumentError = false; + bool isInternalError = false; + string errorMessage = ""; + + try { + _logger.LogInformation($"Attempting to get follow up comments for plan# {planNumber}"); + + if (planNumber <= 0) throw new ArgumentException($"{planNumber} is not a valid PCRB Plan#"); + + List comments = (await _pcrbService.GetFollowUpCommentsByPlanNumber(planNumber, bypassCache)).ToList(); + + return Ok(comments); + } catch (ArgumentException ex) { + isArgumentError = true; + errorMessage = ex.Message; + return BadRequest(errorMessage); + } catch (Exception ex) { + isInternalError = true; + errorMessage = $"Cannot get follow up comments for plan# {planNumber}, because {ex.Message}"; + return Problem(errorMessage); + } finally { + string metricName = "GetPCRBFollowUpComments"; + DateTime end = DateTime.Now; + double millisecondsDiff = (end - start).TotalMilliseconds; + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); + } + } + + [HttpPut] + [Route("pcrb/followUpComment")] + public async Task UpdateFollowUpComment(PCRBFollowUpComment comment) { + DateTime start = DateTime.Now; + bool isArgumentError = false; + bool isInternalError = false; + string errorMessage = ""; + + try { + _logger.LogInformation("Attempting to update follow up comment"); + + if (comment is null) throw new ArgumentNullException("comment cannot be null"); + + await _pcrbService.UpdateFollowUpComment(comment); + + return Ok(); + } catch (ArgumentException ex) { + isArgumentError = true; + errorMessage = ex.Message; + return BadRequest(errorMessage); + } catch (Exception ex) { + isInternalError = true; + errorMessage = $"Unable to update follow up comment, because {ex.Message}"; + return Problem(errorMessage); + } finally { + string metricName = "UpdatePCRBFollowUpComment"; + DateTime end = DateTime.Now; + double millisecondsDiff = (end - start).TotalMilliseconds; + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); + } + } + + [HttpDelete] + [Route("pcrb/followUpComment")] + public async Task DeleteFollowUpComment(int id) { + DateTime start = DateTime.Now; + bool isArgumentError = false; + bool isInternalError = false; + string errorMessage = ""; + + try { + _logger.LogInformation("Attempting to delete follow up comment"); + + if (id <= 0) throw new ArgumentException($"{id} is not a valid PCRB follow up comment ID"); + + await _pcrbService.DeleteFollowUpComment(id); + + return Ok(); + } catch (ArgumentException ex) { + isArgumentError = true; + errorMessage = ex.Message; + return BadRequest(errorMessage); + } catch (Exception ex) { + isInternalError = true; + errorMessage = $"Unable to delete follow up comment, because {ex.Message}"; + return Problem(errorMessage); + } finally { + string metricName = "DeletePCRBFollowUpComment"; + DateTime end = DateTime.Now; + double millisecondsDiff = (end - start).TotalMilliseconds; + + _monInUtils.PostMetrics(metricName, millisecondsDiff, isArgumentError, isInternalError); + } + } } \ No newline at end of file diff --git a/MesaFabApproval.API/Services/ApprovalService.cs b/MesaFabApproval.API/Services/ApprovalService.cs index 5f35e21..69da048 100644 --- a/MesaFabApproval.API/Services/ApprovalService.cs +++ b/MesaFabApproval.API/Services/ApprovalService.cs @@ -13,6 +13,7 @@ public interface IApprovalService { Task> GetApprovalGroupMembers(int subRoleId); Task CreateApproval(Approval approval); Task UpdateApproval(Approval approval); + Task DeleteApproval(int approvalID); Task Approve(Approval approval); Task Deny(Approval approval); Task> GetApprovalsForIssueId(int issueId, bool bypassCache); @@ -40,12 +41,11 @@ public class ApprovalService : IApprovalService { StringBuilder queryBuilder = new(); queryBuilder.Append("insert into Approval (IssueID, RoleName, SubRole, UserID, SubRoleID, ItemStatus, "); - queryBuilder.Append("AssignedDate, DocumentTypeID, DisplayDeniedDocument, Step, TaskID) "); - queryBuilder.Append($"values ({approval.IssueID}, '{approval.RoleName}', '{approval.SubRole}', {approval.UserID}, "); - queryBuilder.Append($"{approval.SubRoleID}, 0, '{approval.AssignedDate.ToString("yyyy-MM-dd HH:mm:ss")}', "); - queryBuilder.Append($"3, 0, {approval.Step}, {approval.TaskID});"); + queryBuilder.Append("AssignedDate, DocumentTypeID, DisplayDeniedDocument, Step, TaskID, CompletedDate) "); + queryBuilder.Append("values (@IssueID, @RoleName, @SubRole, @UserID, @SubRoleID, 0, @AssignedDate, 3, 0, @Step, "); + queryBuilder.Append("@TaskID, @CompletedDate)"); - int rowsCreated = await _dalService.ExecuteAsync(queryBuilder.ToString()); + int rowsCreated = await _dalService.ExecuteAsync(queryBuilder.ToString(), approval); if (rowsCreated <= 0) throw new Exception("Unable to insert approval in database"); @@ -70,19 +70,16 @@ public class ApprovalService : IApprovalService { if (approvals is null || approvals.Count() == 0) { StringBuilder queryBuilder = new(); queryBuilder.Append("select a.*, src.SubRoleCategoryItem from Approval a "); - queryBuilder.Append("join SubRole sr on a.SubRoleID=sr.SubRoleID "); - queryBuilder.Append("join SubRoleCategory src on sr.SubRoleCategoryID=src.SubRoleCategoryID "); + queryBuilder.Append("left outer join SubRole sr on a.SubRoleID=sr.SubRoleID "); + queryBuilder.Append("left outer join SubRoleCategory src on sr.SubRoleCategoryID=src.SubRoleCategoryID "); queryBuilder.Append($"where a.IssueID={issueId}"); approvals = (await _dalService.QueryAsync(queryBuilder.ToString())).ToList(); foreach (Approval approval in approvals) { - int successfulUpdates = 0; - User? user = await _userService.GetUserByUserId(approval.UserID); if (user is not null) { approval.User = user; - successfulUpdates++; } if (approval.ItemStatus < 0) @@ -91,6 +88,9 @@ public class ApprovalService : IApprovalService { approval.StatusMessage = "Assigned"; if (approval.ItemStatus > 0) approval.StatusMessage = "Approved"; + + if (string.IsNullOrWhiteSpace(approval.SubRoleCategoryItem)) + approval.SubRoleCategoryItem = approval.RoleName; } _cache.Set($"approvals{issueId}", approvals, DateTimeOffset.Now.AddMinutes(5)); @@ -217,8 +217,8 @@ public class ApprovalService : IApprovalService { if (approvals is null) { StringBuilder queryBuilder = new(); queryBuilder.Append($"select a.*, src.SubRoleCategoryItem from Approval a "); - queryBuilder.Append("join SubRole sr on a.SubRoleID=sr.SubRoleID "); - queryBuilder.Append("join SubRoleCategory src on sr.SubRoleCategoryID=src.SubRoleCategoryID "); + queryBuilder.Append("left outer join SubRole sr on a.SubRoleID=sr.SubRoleID "); + queryBuilder.Append("left outer join SubRoleCategory src on sr.SubRoleCategoryID=src.SubRoleCategoryID "); queryBuilder.Append($"where UserID={userId} and ItemStatus=0 and "); queryBuilder.Append($"(AssignedDate is not null and "); queryBuilder.Append($"AssignedDate <= '{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}' and "); @@ -265,6 +265,23 @@ public class ApprovalService : IApprovalService { } } + public async Task DeleteApproval(int approvalID) { + try { + _logger.LogInformation($"Attempting to delete approval with ID: {approvalID}"); + + if (approvalID <= 0) throw new ArgumentException("Invalid approval ID"); + + string sql = "delete from Approval where ApprovalID=@ApprovalID"; + + int rowsDeleted = await _dalService.ExecuteAsync(sql, new { ApprovalID = approvalID }); + + if (rowsDeleted <= 0) throw new Exception("unable to delete approval from database"); + } catch (Exception ex) { + _logger.LogError($"Unable to delete approval with ID: {approvalID}, because {ex.Message}"); + throw; + } + } + public async Task Approve(Approval approval) { try { _logger.LogInformation("Attempting to submit approval"); diff --git a/MesaFabApproval.API/Services/AuthenticationService.cs b/MesaFabApproval.API/Services/AuthenticationService.cs index d7ecb8a..c656092 100644 --- a/MesaFabApproval.API/Services/AuthenticationService.cs +++ b/MesaFabApproval.API/Services/AuthenticationService.cs @@ -144,7 +144,7 @@ public class AuthenticationService : IAuthenticationService { Audience = _jwtAudience, Subject = identity, NotBefore = DateTime.Now, - Expires = DateTime.Now.AddHours(8), + Expires = DateTime.Now.AddDays(1), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; diff --git a/MesaFabApproval.API/Services/PCRBService.cs b/MesaFabApproval.API/Services/PCRBService.cs index 0317da6..4e17bf1 100644 --- a/MesaFabApproval.API/Services/PCRBService.cs +++ b/MesaFabApproval.API/Services/PCRBService.cs @@ -34,6 +34,7 @@ public interface IPCRBService { Task UpdatePCR3Document(PCR3Document document); Task> GetPCR3DocumentsForPlanNumber(int planNumber, bool bypassCache); Task NotifyNewApprovals(PCRB pcrb); + Task NotifyApprover(PCRBNotification notification); Task NotifyApprovers(PCRBNotification notification); Task NotifyOriginator(PCRBNotification notification); Task NotifyResponsiblePerson(PCRBActionItemNotification notification); @@ -41,6 +42,10 @@ public interface IPCRBService { Task> GetFollowUpsByPlanNumber(int planNumber, bool bypassCache); Task UpdateFollowUp(PCRBFollowUp followUp); Task DeleteFollowUp(int id); + Task CreateFollowUpComment(PCRBFollowUpComment comment); + Task> GetFollowUpCommentsByPlanNumber(int planNumber, bool bypassCache); + Task UpdateFollowUpComment(PCRBFollowUpComment comment); + Task DeleteFollowUpComment(int id); } public class PCRBService : IPCRBService { @@ -110,6 +115,7 @@ public class PCRBService : IPCRBService { foreach (PCRB pcrb in allPCRBs) { if (string.IsNullOrWhiteSpace(pcrb.OwnerName) && pcrb.OwnerID > 0) pcrb.OwnerName = (await _userService.GetUserByUserId(pcrb.OwnerID)).GetFullName(); + pcrb.FollowUps = await GetFollowUpsByPlanNumber(pcrb.PlanNumber, bypassCache); } _cache.Set("allPCRBs", allPCRBs, DateTimeOffset.Now.AddHours(1)); @@ -144,6 +150,8 @@ public class PCRBService : IPCRBService { if (string.IsNullOrWhiteSpace(pcrb.OwnerName) && pcrb.OwnerID > 0) pcrb.OwnerName = (await _userService.GetUserByUserId(pcrb.OwnerID)).GetFullName(); + pcrb.FollowUps = await GetFollowUpsByPlanNumber(pcrb.PlanNumber, bypassCache); + _cache.Set($"pcrb{planNumber}", pcrb, DateTimeOffset.Now.AddHours(1)); _cache.Set($"pcrb{pcrb.Title}", pcrb, DateTimeOffset.Now.AddHours(1)); } @@ -659,6 +667,62 @@ public class PCRBService : IPCRBService { } } + public async Task NotifyApprover(PCRBNotification notification) { + try { + _logger.LogInformation("Attempting to send a notification to an approver"); + + if (notification is null) throw new ArgumentNullException("notification cannot be null"); + if (notification.PCRB is null) throw new ArgumentNullException("PCRB cannot be null"); + if (notification.Approval is null) throw new ArgumentNullException("approval cannot be null"); + + User user = await _userService.GetUserByUserId(notification.Approval.UserID); + + List toAddresses = new(); + toAddresses.Add(new MailAddress(user.Email)); + + List ccEmails = new List(); + + if (notification.NotifyQaPreApprover) { + IEnumerable qaPreApprovers = await GetQAPreApprovers(); + + foreach (User qaPreApprover in qaPreApprovers) { + if (!ccEmails.Contains(qaPreApprover.Email)) + ccEmails.Add(qaPreApprover.Email); + } + } + + List ccAddresses = new(); + foreach (string email in ccEmails) { + ccAddresses.Add(new MailAddress(email)); + } + + StringBuilder sb = new(); + + string subject = string.Empty; + if (!string.IsNullOrWhiteSpace(notification.Subject)) { + subject = notification.Subject; + } else { + sb.Append($"[Approval Update] Mesa Fab Approval - PCRB# {notification.PCRB.PlanNumber} - "); + sb.Append($"{notification.PCRB.Title}"); + + subject = sb.ToString(); + } + + sb.Clear(); + sb.Append($"{notification.Message}

"); + sb.Append($"Click {_siteBaseUrl}/redirect?redirectPath=pcrb/{notification.PCRB.PlanNumber} "); + sb.Append("to view the PCRB."); + + await _smtpService.SendEmail(toAddresses, ccAddresses, subject, sb.ToString()); + + notification.Approval.NotifyDate = DateTime.Now; + await _approvalService.UpdateApproval(notification.Approval); + } catch (Exception ex) { + _logger.LogError($"Unable to send notification to approver, because {ex.Message}"); + throw; + } + } + public async Task NotifyApprovers(PCRBNotification notification) { try { _logger.LogInformation("Attempting to send notification to approvers"); @@ -712,9 +776,28 @@ public class PCRBService : IPCRBService { List toAddresses = new(); toAddresses.Add(new MailAddress(user.Email)); - List ccAddresses = new(); + List ccEmails = new List(); - string subject = $"[Update] Mesa Fab Approval - PCRB# {notification.PCRB.PlanNumber} - {notification.PCRB.Title}"; + if (notification.NotifyQaPreApprover) { + IEnumerable qaPreApprovers = await GetQAPreApprovers(); + + foreach (User qaPreApprover in qaPreApprovers) { + if (!ccEmails.Contains(qaPreApprover.Email)) + ccEmails.Add(qaPreApprover.Email); + } + } + + List ccAddresses = new(); + foreach (string email in ccEmails) { + ccAddresses.Add(new MailAddress(email)); + } + + string subject = string.Empty; + if (!string.IsNullOrWhiteSpace(notification.Subject)) { + subject = notification.Subject; + } else { + subject = $"[Update] Mesa Fab Approval - PCRB# {notification.PCRB.PlanNumber} - {notification.PCRB.Title}"; + } StringBuilder bodyBuilder = new(); bodyBuilder.Append($"{notification.Message}

"); @@ -763,8 +846,8 @@ public class PCRBService : IPCRBService { if (followUp is null) throw new ArgumentNullException("follow up cannot be null"); StringBuilder queryBuilder = new(); - queryBuilder.Append("insert into CCPCRBFollowUp (PlanNumber, Step, FollowUpDate, CompletedDate) "); - queryBuilder.Append("values (@PlanNumber, @Step, @FollowUpDate, @CompletedDate)"); + queryBuilder.Append("insert into CCPCRBFollowUp (PlanNumber, Step, FollowUpDate, CompletedDate, UpdateDate) "); + queryBuilder.Append("values (@PlanNumber, @Step, @FollowUpDate, @CompletedDate, @UpdateDate)"); int rowsReturned = await _dalService.ExecuteAsync(queryBuilder.ToString(), followUp); @@ -792,7 +875,7 @@ public class PCRBService : IPCRBService { followUps = await _dalService.QueryAsync(sql, new { PlanNumber = planNumber }); if (followUps is not null) - _cache.Set($"pcrbFollowUps{planNumber}", followUps, DateTimeOffset.Now.AddMinutes(15)); + _cache.Set($"pcrbFollowUps{planNumber}", followUps, DateTimeOffset.Now.AddHours(1)); } return followUps ?? new List(); @@ -811,7 +894,8 @@ public class PCRBService : IPCRBService { StringBuilder queryBuilder = new(); queryBuilder.Append("update CCPCRBFollowUp set Step=@Step, FollowUpDate=@FollowUpDate, IsComplete=@IsComplete, "); - queryBuilder.Append("IsDeleted=@IsDeleted, CompletedDate=@CompletedDate, Comments=@Comments "); + queryBuilder.Append("IsDeleted=@IsDeleted, CompletedDate=@CompletedDate, IsPendingApproval=@IsPendingApproval, "); + queryBuilder.Append("UpdateDate=@UpdateDate "); queryBuilder.Append("where ID=@ID"); int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString(), followUp); @@ -840,6 +924,89 @@ public class PCRBService : IPCRBService { } } + public async Task CreateFollowUpComment(PCRBFollowUpComment comment) { + try { + _logger.LogInformation("Attempting to create PCRB follow up"); + + if (comment is null) throw new ArgumentNullException("comment cannot be null"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append("insert into CCPCRBFollowUpComments (PlanNumber, FollowUpID, Comment, CommentDate, UserID) "); + queryBuilder.Append("values (@PlanNumber, @FollowUpID, @Comment, @CommentDate, @UserID)"); + + int rowsReturned = await _dalService.ExecuteAsync(queryBuilder.ToString(), comment); + + if (rowsReturned <= 0) throw new Exception("unable to insert new follow up comment in the database"); + } catch (Exception ex) { + _logger.LogError($"Unable to create new follow up comment, because {ex.Message}"); + throw; + } + } + + public async Task> GetFollowUpCommentsByPlanNumber(int planNumber, bool bypassCache) { + try { + _logger.LogInformation($"Attempting to fetch follow up comments for PCRB {planNumber}"); + + if (planNumber <= 0) throw new ArgumentException($"{planNumber} is not a valid PCRB Plan#"); + + IEnumerable? comments = new List(); + + if (!bypassCache) + comments = _cache.Get>($"pcrbFollowUpComments{planNumber}"); + + if (comments is null || comments.Count() == 0) { + string sql = "select * from CCPCRBFollowUpComments where PlanNumber=@PlanNumber"; + + comments = await _dalService.QueryAsync(sql, new { PlanNumber = planNumber }); + + if (comments is not null) + _cache.Set($"pcrbFollowUpComments{planNumber}", comments, DateTimeOffset.Now.AddHours(1)); + } + + return comments ?? new List(); + } catch (Exception ex) { + _logger.LogError($"Unable to fetch follow up comments for PCRB {planNumber}, because {ex.Message}"); + throw; + } + } + + public async Task UpdateFollowUpComment(PCRBFollowUpComment comment) { + try { + _logger.LogInformation("Attempting to update a follow up"); + + if (comment is null) + throw new ArgumentNullException("comment cannot be null"); + + StringBuilder queryBuilder = new(); + queryBuilder.Append("update CCPCRBFollowUpComments set Comment=@Comment, CommentDate=@CommentDate, "); + queryBuilder.Append("UserID=@UserID where ID=@ID"); + + int rowsAffected = await _dalService.ExecuteAsync(queryBuilder.ToString(), comment); + + if (rowsAffected <= 0) throw new Exception("update failed in database"); + } catch (Exception ex) { + _logger.LogError($"Unable to update follow up comment, because {ex.Message}"); + throw; + } + } + + public async Task DeleteFollowUpComment(int id) { + try { + _logger.LogInformation($"Attempting to delete follow up comment {id}"); + + if (id <= 0) throw new ArgumentException($"{id} is not a valid follow up ID"); + + string sql = "delete from CCPCRBFollowUpComments where ID=@ID"; + + int rowsAffected = await _dalService.ExecuteAsync(sql, new { ID = id }); + + if (rowsAffected <= 0) throw new Exception("delete operation failed in database"); + } catch (Exception ex) { + _logger.LogError($"Unable to delete follow up comment {id}, because {ex.Message}"); + throw; + } + } + private async Task SaveAttachmentInDb(IFormFile file, PCRBAttachment attachment) { try { _logger.LogInformation($"Attempting to save attachment to database"); @@ -863,4 +1030,34 @@ public class PCRBService : IPCRBService { throw; } } + + private async Task> GetQAPreApprovers() { + try { + _logger.LogInformation("Attempting to fetch QA Pre-Approvers"); + + IEnumerable qaPreApprovers = new List(); + + int qaPreApproverRoleId = await _approvalService.GetRoleIdForRoleName("QA_PRE_APPROVAL"); + + if (qaPreApproverRoleId > 0) { + IEnumerable qaPreApproverSubRoles = + await _approvalService.GetSubRolesForSubRoleName("QA_PRE_APPROVAL", qaPreApproverRoleId); + + foreach (SubRole subRole in qaPreApproverSubRoles) { + IEnumerable members = + await _approvalService.GetApprovalGroupMembers(subRole.SubRoleID); + + foreach (User member in members) { + if (!qaPreApprovers.Any(u => u.UserID == member.UserID)) + qaPreApprovers = qaPreApprovers.Append(member); + } + } + } + + return qaPreApprovers; + } catch (Exception ex) { + _logger.LogError($"Unable to fetch QA Pre-Approvers, because {ex.Message}"); + throw; + } + } } \ No newline at end of file diff --git a/MesaFabApproval.Client.Test/ApprovalServiceTests.cs b/MesaFabApproval.Client.Test/ApprovalServiceTests.cs new file mode 100644 index 0000000..4738055 --- /dev/null +++ b/MesaFabApproval.Client.Test/ApprovalServiceTests.cs @@ -0,0 +1,95 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; + +using Moq; +using Moq.Protected; + +using Xunit; + +namespace MesaFabApproval.Client.Test; + +public class ApprovalServiceTests { + private readonly Mock _cacheMock; + private readonly Mock _httpClientFactoryMock; + private readonly ApprovalService _approvalService; + + public ApprovalServiceTests() { + _cacheMock = new Mock(); + _httpClientFactoryMock = new Mock(); + _approvalService = new ApprovalService(_cacheMock.Object, _httpClientFactoryMock.Object); + } + + [Fact] + public async Task DeleteApproval_ValidApprovalID_DeletesApproval() { + int approvalID = 1; + + Mock handlerMock = new Mock(); + + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(req => req.Method == HttpMethod.Delete), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage { + StatusCode = HttpStatusCode.OK, + }); + + HttpClient httpClient = new HttpClient(handlerMock.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _approvalService.DeleteApproval(approvalID); + + handlerMock.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is(req => req.Method == HttpMethod.Delete), + ItExpr.IsAny() + ); + } + + [Fact] + public async Task DeleteApproval_InvalidApprovalID_ThrowsArgumentException() { + int approvalID = 0; + + await Assert.ThrowsAsync(() => _approvalService.DeleteApproval(approvalID)); + } + + [Fact] + public async Task DeleteApproval_DeletionFails_ThrowsException() { + int approvalID = 1; + + Mock handlerMock = new Mock(); + handlerMock + .Protected() + .Setup>( + "SendAsync", + ItExpr.Is(req => req.Method == HttpMethod.Delete), + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage { + StatusCode = HttpStatusCode.BadRequest, + ReasonPhrase = "Bad Request" + }); + + HttpClient httpClient = new HttpClient(handlerMock.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _httpClientFactoryMock.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _approvalService.DeleteApproval(approvalID)); + } +} diff --git a/MesaFabApproval.Client.Test/MockMemoryCacheService.cs b/MesaFabApproval.Client.Test/MockMemoryCacheService.cs new file mode 100644 index 0000000..441291b --- /dev/null +++ b/MesaFabApproval.Client.Test/MockMemoryCacheService.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Caching.Memory; + +using Moq; + +namespace MesaFabApproval.Client.Test; + +public static class MockMemoryCacheService { + public static Mock GetMemoryCache(object expectedValue) { + Mock mockMemoryCache = new Mock(); + mockMemoryCache + .Setup(x => x.TryGetValue(It.IsAny(), out expectedValue)) + .Returns(true); + mockMemoryCache + .Setup(x => x.CreateEntry(It.IsAny())) + .Returns(Mock.Of()); + return mockMemoryCache; + } +} diff --git a/MesaFabApproval.Client.Test/PCRBFollowUpCommentTests.cs b/MesaFabApproval.Client.Test/PCRBFollowUpCommentTests.cs new file mode 100644 index 0000000..228bb9c --- /dev/null +++ b/MesaFabApproval.Client.Test/PCRBFollowUpCommentTests.cs @@ -0,0 +1,320 @@ +using System.Net; +using System.Text.Json; + +using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; + +using Moq; +using Moq.Protected; + +using MudBlazor; + +namespace MesaFabApproval.Client.Test; + +public class PCRBFollowUpCommentTests { + private readonly Mock _mockCache; + private readonly Mock _mockHttpClientFactory; + private readonly Mock _mockSnackbar; + private readonly Mock _mockUserService; + private readonly PCRBService _pcrbService; + + private static PCRBFollowUpComment FOLLOW_UP_COMMENT = new PCRBFollowUpComment { + ID = 1, + PlanNumber = 123, + FollowUpID = 1, + Comment = "Test Comment", + UserID = 1 + }; + + private static HttpResponseMessage GET_RESPONSE = new HttpResponseMessage { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(new List { FOLLOW_UP_COMMENT })) + }; + + private static IEnumerable FOLLOW_UPS_COMMENTS = new List() { FOLLOW_UP_COMMENT }; + + private static HttpResponseMessage SUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.OK); + private static HttpResponseMessage UNSUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.InternalServerError); + + public PCRBFollowUpCommentTests() { + _mockCache = MockMemoryCacheService.GetMemoryCache(FOLLOW_UPS_COMMENTS); + _mockHttpClientFactory = new Mock(); + _mockSnackbar = new Mock(); + _mockUserService = new Mock(); + + _pcrbService = new PCRBService( + _mockCache.Object, + _mockHttpClientFactory.Object, + _mockSnackbar.Object, + _mockUserService.Object); + } + + [Fact] + public async Task CreateFollowUpComment_WithValidParams_ShouldCallHttpPost_AndRefreshCache() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Post), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(GET_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.CreateFollowUpComment(FOLLOW_UP_COMMENT); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Post && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUpComment")), + ItExpr.IsAny()); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUpComments?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + } + + [Fact] + public async Task CreateFollowUpComment_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Post), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUpComment(FOLLOW_UP_COMMENT)); + } + + [Fact] + public async Task CreateFollowUpComment_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUpComment(null)); + } + + [Fact] + public async Task GetFollowUpCommentsByPlanNumber_WithBypassCache_ShouldCallHttpGetAndReturnFollowUps() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(GET_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + IEnumerable comments = await _pcrbService.GetFollowUpCommentsByPlanNumber(123, true); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUpComments?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + + Assert.Single(comments); + } + + [Fact] + public async Task GetFollowUpCommentsByPlanNumber_WithoutBypassCache_ShouldReturnFollowUpsFromCache() { + IEnumerable comments = await _pcrbService.GetFollowUpCommentsByPlanNumber(1, false); + Assert.Single(comments); + } + + [Fact] + public async Task GetFollowUpCommentsByPlanNumber_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpCommentsByPlanNumber(1, true)); + } + + [Fact] + public async Task UpdateFollowUpComment_WithValidParams_ShouldCallHttpPut() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Put), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(GET_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.UpdateFollowUpComment(FOLLOW_UP_COMMENT); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Put && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUpComment")), + ItExpr.IsAny()); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUpComments?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + } + + [Fact] + public async Task UpdateFollowUpComment_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUpComment(null)); + } + + [Fact] + public async Task UpdateFollowUpComment_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Put), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUpComment(FOLLOW_UP_COMMENT)); + } + + [Fact] + public async Task DeleteFollowUpComment_WithValidParams_ShouldCallHttpDelete() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Delete), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.DeleteFollowUpComment(1); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Delete && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUpComment?id=1")), + ItExpr.IsAny()); + } + + [Fact] + public async Task DeleteFollowUpComment_WithBadId_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUpComment(0)); + } + + [Fact] + public async Task DeleteFollowUpComment_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Delete), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUpComment(1)); + } +} diff --git a/MesaFabApproval.Client.Test/PCRBFollowUpTests.cs b/MesaFabApproval.Client.Test/PCRBFollowUpTests.cs new file mode 100644 index 0000000..c11519c --- /dev/null +++ b/MesaFabApproval.Client.Test/PCRBFollowUpTests.cs @@ -0,0 +1,321 @@ +using System.Net; +using System.Text.Json; + +using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.Extensions.Caching.Memory; + +using Moq; +using Moq.Protected; + +using MudBlazor; + +namespace MesaFabApproval.Client.Test; + +public partial class PCRBFollowUpTests { + private readonly Mock _mockCache; + private readonly Mock _mockHttpClientFactory; + private readonly Mock _mockSnackbar; + private readonly Mock _mockUserService; + private readonly PCRBService _pcrbService; + + private static PCRBFollowUp FOLLOW_UP = new PCRBFollowUp { + ID = 1, + PlanNumber = 123, + Step = 1, + FollowUpDate = DateTime.Now + }; + + private static HttpResponseMessage GET_RESPONSE = new HttpResponseMessage { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonSerializer.Serialize(new List { FOLLOW_UP })) + }; + + private static IEnumerable FOLLOW_UPS = new List() { + new PCRBFollowUp { ID = 1, PlanNumber = 1, Step = 1, FollowUpDate = DateTime.Now } + }; + + private static HttpResponseMessage SUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.OK); + private static HttpResponseMessage UNSUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.InternalServerError); + + public PCRBFollowUpTests() { + _mockCache = MockMemoryCacheService.GetMemoryCache(FOLLOW_UPS); + _mockHttpClientFactory = new Mock(); + _mockSnackbar = new Mock(); + _mockUserService = new Mock(); + + _pcrbService = new PCRBService( + _mockCache.Object, + _mockHttpClientFactory.Object, + _mockSnackbar.Object, + _mockUserService.Object); + } + + [Fact] + public async Task CreateFollowUp_WithValidParams_ShouldCallHttpPost_AndRefreshCache() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Post), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(GET_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.CreateFollowUp(FOLLOW_UP); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Post && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp")), + ItExpr.IsAny()); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + } + + [Fact] + public async Task CreateFollowUp_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Post), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(FOLLOW_UP)); + } + + [Fact] + public async Task CreateFollowUp_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(null)); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithBypassCache_ShouldCallHttpGetAndReturnFollowUps() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(GET_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + IEnumerable followUps = await _pcrbService.GetFollowUpsByPlanNumber(123, true); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + + Assert.Single(followUps); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithoutBypassCache_ShouldReturnFollowUpsFromCache() { + IEnumerable followUps = await _pcrbService.GetFollowUpsByPlanNumber(1, false); + Assert.Single(followUps); + } + + [Fact] + public async Task GetFollowUpsByPlanNumber_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpsByPlanNumber(1, true)); + } + + [Fact] + public async Task UpdateFollowUp_WithValidParams_ShouldCallHttpPut() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Put), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Get), + ItExpr.IsAny()) + .ReturnsAsync(GET_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.UpdateFollowUp(FOLLOW_UP); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Put && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp")), + ItExpr.IsAny()); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Get && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), + ItExpr.IsAny()); + } + + [Fact] + public async Task UpdateFollowUp_WithNullParam_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(null)); + } + + [Fact] + public async Task UpdateFollowUp_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Put), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(FOLLOW_UP)); + } + + [Fact] + public async Task DeleteFollowUp_WithValidParams_ShouldCallHttpDelete() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Delete), + ItExpr.IsAny()) + .ReturnsAsync(SUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await _pcrbService.DeleteFollowUp(1); + + mockHttpMessageHandler.Protected().Verify( + "SendAsync", + Times.Once(), + ItExpr.Is( + req => + req.Method == HttpMethod.Delete && + req.RequestUri != null && + req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp?id=1")), + ItExpr.IsAny()); + } + + [Fact] + public async Task DeleteFollowUp_WithBadId_ShouldThrowException() { + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(0)); + } + + [Fact] + public async Task DeleteFollowUp_WithBadResponse_ShouldThrowException() { + Mock mockHttpMessageHandler = new Mock(); + + mockHttpMessageHandler.Protected() + .Setup>( + "SendAsync", + ItExpr.Is(_ => _.Method == HttpMethod.Delete), + ItExpr.IsAny()) + .ReturnsAsync(UNSUCCESSFUL_RESPONSE) + .Verifiable(); + + HttpClient httpClient = new HttpClient(mockHttpMessageHandler.Object) { + BaseAddress = new Uri("https://localhost:5000") + }; + + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + + await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(1)); + } +} diff --git a/MesaFabApproval.Client.Test/PCRBServiceTests.cs b/MesaFabApproval.Client.Test/PCRBServiceTests.cs index 7073309..e3c8935 100644 --- a/MesaFabApproval.Client.Test/PCRBServiceTests.cs +++ b/MesaFabApproval.Client.Test/PCRBServiceTests.cs @@ -1,5 +1,6 @@ using System.Net; -using System.Text.Json; +using System.Net.Http; +using System.Threading.Tasks; using MesaFabApproval.Client.Services; using MesaFabApproval.Shared.Models; @@ -11,6 +12,8 @@ using Moq.Protected; using MudBlazor; +using Xunit; + namespace MesaFabApproval.Client.Test; public class PCRBServiceTests { @@ -18,330 +21,136 @@ public class PCRBServiceTests { private readonly Mock _mockHttpClientFactory; private readonly Mock _mockSnackbar; private readonly Mock _mockUserService; + private readonly Mock _mockPCRB; + private readonly Mock _mockApproval; + private readonly PCRBService _pcrbService; - private static IEnumerable FOLLOW_UPS = new List() { - new PCRBFollowUp { ID = 1, PlanNumber = 1, Step = 1, FollowUpDate = DateTime.Now } - }; - - private static HttpResponseMessage SUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.OK); - private static HttpResponseMessage UNSUCCESSFUL_RESPONSE = new HttpResponseMessage(HttpStatusCode.InternalServerError); - - public static class MockMemoryCacheService { - public static Mock GetMemoryCache(object expectedValue) { - Mock mockMemoryCache = new Mock(); - mockMemoryCache - .Setup(x => x.TryGetValue(It.IsAny(), out expectedValue)) - .Returns(true); - mockMemoryCache - .Setup(x => x.CreateEntry(It.IsAny())) - .Returns(Mock.Of()); - return mockMemoryCache; - } - } - public PCRBServiceTests() { - _mockCache = MockMemoryCacheService.GetMemoryCache(FOLLOW_UPS); + _mockCache = new Mock(); _mockHttpClientFactory = new Mock(); _mockSnackbar = new Mock(); _mockUserService = new Mock(); + _mockPCRB = new Mock(); + _mockApproval = new Mock(); _pcrbService = new PCRBService( _mockCache.Object, _mockHttpClientFactory.Object, _mockSnackbar.Object, - _mockUserService.Object); + _mockUserService.Object + ); } [Fact] - public async Task CreateFollowUp_WithValidParams_ShouldCallHttpPost_AndRefreshCache() { - PCRBFollowUp followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 123, - Step = 1, - FollowUpDate = DateTime.Now, - Comments = "Test" + public async Task NotifyApprover_ShouldSendNotification() { + PCRBNotification notification = new PCRBNotification { + Message = "Test Message", + PCRB = _mockPCRB.Object, + Approval = _mockApproval.Object }; - HttpResponseMessage getResponse = new HttpResponseMessage { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(new List { followUp })) - }; - - var mockHttpMessageHandler = new Mock(); - mockHttpMessageHandler.Protected() + Mock handlerMock = new Mock(); + handlerMock + .Protected() .Setup>( "SendAsync", ItExpr.Is(_ => _.Method == HttpMethod.Post), - ItExpr.IsAny()) - .ReturnsAsync(SUCCESSFUL_RESPONSE) + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage { + StatusCode = HttpStatusCode.OK + }) .Verifiable(); - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Get), - ItExpr.IsAny()) - .ReturnsAsync(getResponse) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + HttpClient httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri("https://localhost:5000") }; + _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - await _pcrbService.CreateFollowUp(followUp); + await _pcrbService.NotifyApprover(notification); - mockHttpMessageHandler.Protected().Verify( + handlerMock.Protected().Verify( "SendAsync", Times.Once(), - ItExpr.Is( - req => - req.Method == HttpMethod.Post && - req.RequestUri != null && - req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp")), - ItExpr.IsAny()); - - mockHttpMessageHandler.Protected().Verify( - "SendAsync", - Times.Once(), - ItExpr.Is( - req => - req.Method == HttpMethod.Get && - req.RequestUri != null && - req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), - ItExpr.IsAny()); + ItExpr.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri == new Uri("https://localhost:5000/pcrb/notify/approver") + ), + ItExpr.IsAny() + ); } [Fact] - public async Task CreateFollowUp_WithBadResponse_ShouldThrowException() { - PCRBFollowUp followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 123, - Step = 1, - FollowUpDate = DateTime.Now, - Comments = "Test" + public async Task NotifyApprover_ShouldThrowException_WhenResponseIsNotSuccess() { + PCRBNotification notification = new PCRBNotification { + Message = "Test Message", + PCRB = _mockPCRB.Object, + Approval = _mockApproval.Object }; - var mockHttpMessageHandler = new Mock(); - mockHttpMessageHandler.Protected() + var handlerMock = new Mock(); + handlerMock + .Protected() .Setup>( "SendAsync", ItExpr.Is(_ => _.Method == HttpMethod.Post), - ItExpr.IsAny()) - .ReturnsAsync(UNSUCCESSFUL_RESPONSE) - .Verifiable(); + ItExpr.IsAny() + ) + .ReturnsAsync(new HttpResponseMessage { + StatusCode = HttpStatusCode.BadRequest, + ReasonPhrase = "Bad Request" + }); - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { - BaseAddress = new Uri("https://localhost:5000") - }; - _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - - await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(followUp)); - } - - [Fact] - public async Task CreateFollowUp_WithNullParam_ShouldThrowException() { - await Assert.ThrowsAsync(() => _pcrbService.CreateFollowUp(null)); - } - - [Fact] - public async Task GetFollowUpsByPlanNumber_WithBypassCache_ShouldCallHttpGetAndReturnFollowUps() { - PCRBFollowUp followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 123, - Step = 1, - FollowUpDate = DateTime.Now, - Comments = "Test" - }; - - HttpResponseMessage getResponse = new HttpResponseMessage { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(new List { followUp })) - }; - - var mockHttpMessageHandler = new Mock(); - - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Get), - ItExpr.IsAny()) - .ReturnsAsync(getResponse) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { - BaseAddress = new Uri("https://localhost:5000") - }; - _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - - IEnumerable followUps = await _pcrbService.GetFollowUpsByPlanNumber(123, true); - - mockHttpMessageHandler.Protected().Verify( - "SendAsync", - Times.Once(), - ItExpr.Is( - req => - req.Method == HttpMethod.Get && - req.RequestUri != null && - req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUps?planNumber=123&bypassCache=True")), - ItExpr.IsAny()); - - Assert.Single(followUps); - } - - [Fact] - public async Task GetFollowUpsByPlanNumber_WithoutBypassCache_ShouldReturnFollowUpsFromCache() { - IEnumerable followUps = await _pcrbService.GetFollowUpsByPlanNumber(1, false); - Assert.Single(followUps); - } - - [Fact] - public async Task GetFollowUpsByPlanNumber_WithBadResponse_ShouldThrowException() { - var mockHttpMessageHandler = new Mock(); - - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Get), - ItExpr.IsAny()) - .ReturnsAsync(UNSUCCESSFUL_RESPONSE) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { + HttpClient httpClient = new HttpClient(handlerMock.Object) { BaseAddress = new Uri("https://localhost:5000") }; _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - await Assert.ThrowsAsync(() => _pcrbService.GetFollowUpsByPlanNumber(1, true)); + Exception exception = await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); + Assert.Equal("Unable to notify PCRB approver, because Bad Request", exception.Message); } [Fact] - public async Task UpdateFollowUp_WithValidParams_ShouldCallHttpPut() { - PCRBFollowUp followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 123, - Step = 1, - FollowUpDate = DateTime.Now, - Comments = "Test" - }; - - var mockHttpMessageHandler = new Mock(); - - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Put), - ItExpr.IsAny()) - .ReturnsAsync(SUCCESSFUL_RESPONSE) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { - BaseAddress = new Uri("https://localhost:5000") - }; - _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - - await _pcrbService.UpdateFollowUp(followUp); - - mockHttpMessageHandler.Protected().Verify( - "SendAsync", - Times.Once(), - ItExpr.Is( - req => - req.Method == HttpMethod.Put && - req.RequestUri != null && - req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp")), - ItExpr.IsAny()); + public async Task NotifyApprover_ShouldThrowException_WhenNotificationIsNull() { + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(null)); } [Fact] - public async Task UpdateFollowUp_WithNullParam_ShouldThrowException() { - await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(null)); + public async Task NotifyApprover_ShouldThrowException_WhenPCRBIsNull() { + PCRBNotification notification = new PCRBNotification { + Message = "Test Message", + PCRB = null, + Approval = _mockApproval.Object + }; + + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); } [Fact] - public async Task UpdateFollowUp_WithBadResponse_ShouldThrowException() { - PCRBFollowUp followUp = new PCRBFollowUp { - ID = 1, - PlanNumber = 123, - Step = 1, - FollowUpDate = DateTime.Now, - Comments = "Test" + public async Task NotifyApprover_ShouldThrowException_WhenApprovalIsNull() { + PCRBNotification notification = new PCRBNotification { + Message = "Test Message", + PCRB = _mockPCRB.Object, + Approval = null }; - var mockHttpMessageHandler = new Mock(); - - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Put), - ItExpr.IsAny()) - .ReturnsAsync(UNSUCCESSFUL_RESPONSE) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { - BaseAddress = new Uri("https://localhost:5000") - }; - - _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - - await Assert.ThrowsAsync(() => _pcrbService.UpdateFollowUp(followUp)); + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); } [Fact] - public async Task DeleteFollowUp_WithValidParams_ShouldCallHttpDelete() { - var mockHttpMessageHandler = new Mock(); - - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Delete), - ItExpr.IsAny()) - .ReturnsAsync(SUCCESSFUL_RESPONSE) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { - BaseAddress = new Uri("https://localhost:5000") - }; - _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); - - await _pcrbService.DeleteFollowUp(1); - - mockHttpMessageHandler.Protected().Verify( - "SendAsync", - Times.Once(), - ItExpr.Is( - req => - req.Method == HttpMethod.Delete && - req.RequestUri != null && - req.RequestUri.AbsoluteUri.Equals("https://localhost:5000/pcrb/followUp?id=1")), - ItExpr.IsAny()); - } - - [Fact] - public async Task DeleteFollowUp_WithBadId_ShouldThrowException() { - await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(0)); - } - - [Fact] - public async Task DeleteFollowUp_WithBadResponse_ShouldThrowException() { - var mockHttpMessageHandler = new Mock(); - - mockHttpMessageHandler.Protected() - .Setup>( - "SendAsync", - ItExpr.Is(_ => _.Method == HttpMethod.Delete), - ItExpr.IsAny()) - .ReturnsAsync(UNSUCCESSFUL_RESPONSE) - .Verifiable(); - - var httpClient = new HttpClient(mockHttpMessageHandler.Object) { - BaseAddress = new Uri("https://localhost:5000") + public async Task NotifyApprover_ShouldThrowException_WhenMessageIsNullOrEmpty() { + PCRBNotification notification = new PCRBNotification { + Message = null, + PCRB = _mockPCRB.Object, + Approval = _mockApproval.Object }; - _mockHttpClientFactory.Setup(_ => _.CreateClient(It.IsAny())).Returns(httpClient); + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); - await Assert.ThrowsAsync(() => _pcrbService.DeleteFollowUp(1)); + notification.Message = string.Empty; + await Assert.ThrowsAsync(() => _pcrbService.NotifyApprover(notification)); } } diff --git a/MesaFabApproval.Client/MesaFabApproval.Client.csproj b/MesaFabApproval.Client/MesaFabApproval.Client.csproj index 105c8a6..ef851c4 100644 --- a/MesaFabApproval.Client/MesaFabApproval.Client.csproj +++ b/MesaFabApproval.Client/MesaFabApproval.Client.csproj @@ -15,15 +15,15 @@ - - - - - - + + + + + + - - + + diff --git a/MesaFabApproval.Client/Pages/AuthenticatedRedirect.razor.cs b/MesaFabApproval.Client/Pages/AuthenticatedRedirect.razor.cs index 80b6bdf..37f5ef8 100644 --- a/MesaFabApproval.Client/Pages/AuthenticatedRedirect.razor.cs +++ b/MesaFabApproval.Client/Pages/AuthenticatedRedirect.razor.cs @@ -1,7 +1,9 @@ using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Caching.Memory; using MudBlazor; @@ -13,8 +15,8 @@ public partial class AuthenticatedRedirect { [Inject] MesaFabApprovalAuthStateProvider authStateProvider { get; set; } [Inject] IAuthenticationService authService { get; set; } [Inject] IUserService userService { get; set; } - [Inject] ISnackbar snackbar { get; set; } [Inject] NavigationManager navigationManager { get; set; } + [Inject] IMemoryCache cache { get; set; } private string? _jwt; private string? _refreshToken; @@ -53,13 +55,16 @@ public partial class AuthenticatedRedirect { await authStateProvider.StateHasChanged(principal); } + AuthTokens authTokens = await authService.GetAuthTokens(); + if (authStateProvider.CurrentUser is not null && !string.IsNullOrWhiteSpace(_redirectPath)) { navigationManager.NavigateTo(_redirectPath); } else { await authStateProvider.Logout(); if (!string.IsNullOrWhiteSpace(_redirectPath)) { - navigationManager.NavigateTo($"login/{_redirectPath}"); + cache.Set("redirectUrl", _redirectPath); + navigationManager.NavigateTo($"login?redirectPath={_redirectPath}"); } else { navigationManager.NavigateTo("login"); } @@ -68,7 +73,8 @@ public partial class AuthenticatedRedirect { await authStateProvider.Logout(); if (!string.IsNullOrWhiteSpace(_redirectPath)) { - navigationManager.NavigateTo($"login/{_redirectPath}"); + cache.Set("redirectUrl", _redirectPath); + navigationManager.NavigateTo($"login?redirectPath={_redirectPath}"); } else { navigationManager.NavigateTo("login"); } diff --git a/MesaFabApproval.Client/Pages/Components/Comments.razor b/MesaFabApproval.Client/Pages/Components/Comments.razor index 9ecdaad..f08b6e8 100644 --- a/MesaFabApproval.Client/Pages/Components/Comments.razor +++ b/MesaFabApproval.Client/Pages/Components/Comments.razor @@ -37,7 +37,7 @@ @code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public string comments { get; set; } = ""; diff --git a/MesaFabApproval.Client/Pages/Components/MRBActionCommentsAndFiles.razor b/MesaFabApproval.Client/Pages/Components/MRBActionCommentsAndFiles.razor index 37259de..bd92f8e 100644 --- a/MesaFabApproval.Client/Pages/Components/MRBActionCommentsAndFiles.razor +++ b/MesaFabApproval.Client/Pages/Components/MRBActionCommentsAndFiles.razor @@ -54,7 +54,7 @@ @code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public string comments { get; set; } = ""; diff --git a/MesaFabApproval.Client/Pages/Components/MRBActionForm.razor b/MesaFabApproval.Client/Pages/Components/MRBActionForm.razor index f6a1332..c6c22ff 100644 --- a/MesaFabApproval.Client/Pages/Components/MRBActionForm.razor +++ b/MesaFabApproval.Client/Pages/Components/MRBActionForm.razor @@ -143,7 +143,7 @@ @code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public MRBAction mrbAction { get; set; } diff --git a/MesaFabApproval.Client/Pages/Components/MRBApproverSelector.razor b/MesaFabApproval.Client/Pages/Components/MRBApproverSelector.razor index 026bdf2..19225ce 100644 --- a/MesaFabApproval.Client/Pages/Components/MRBApproverSelector.razor +++ b/MesaFabApproval.Client/Pages/Components/MRBApproverSelector.razor @@ -43,7 +43,7 @@ @code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public User selectedUser { get; set; } diff --git a/MesaFabApproval.Client/Pages/Components/PCR3DocumentForm.razor b/MesaFabApproval.Client/Pages/Components/PCR3DocumentForm.razor index 9d7d349..c0b05f7 100644 --- a/MesaFabApproval.Client/Pages/Components/PCR3DocumentForm.razor +++ b/MesaFabApproval.Client/Pages/Components/PCR3DocumentForm.razor @@ -74,7 +74,7 @@ @code { [CascadingParameter] - MudDialogInstance MudDialog { get; set; } + IMudDialogInstance MudDialog { get; set; } [Parameter] public required PCR3Document document { get; set; } diff --git a/MesaFabApproval.Client/Pages/Components/PCRBActionItemForm.razor b/MesaFabApproval.Client/Pages/Components/PCRBActionItemForm.razor index 72d5c59..4cb4f3d 100644 --- a/MesaFabApproval.Client/Pages/Components/PCRBActionItemForm.razor +++ b/MesaFabApproval.Client/Pages/Components/PCRBActionItemForm.razor @@ -73,7 +73,7 @@ @code { [CascadingParameter] - MudDialogInstance MudDialog { get; set; } + IMudDialogInstance MudDialog { get; set; } [Parameter] public int planNumber { get; set; } = 0; diff --git a/MesaFabApproval.Client/Pages/Components/PCRBApproverForm.razor b/MesaFabApproval.Client/Pages/Components/PCRBApproverForm.razor index 5300779..8e4d2e1 100644 --- a/MesaFabApproval.Client/Pages/Components/PCRBApproverForm.razor +++ b/MesaFabApproval.Client/Pages/Components/PCRBApproverForm.razor @@ -61,7 +61,7 @@ @code { [CascadingParameter] - MudDialogInstance MudDialog { get; set; } + IMudDialogInstance MudDialog { get; set; } [Parameter] public int planNumber { get; set; } = 0; diff --git a/MesaFabApproval.Client/Pages/Components/PCRBAttachmentForm.razor b/MesaFabApproval.Client/Pages/Components/PCRBAttachmentForm.razor index f0e97c7..2e6227a 100644 --- a/MesaFabApproval.Client/Pages/Components/PCRBAttachmentForm.razor +++ b/MesaFabApproval.Client/Pages/Components/PCRBAttachmentForm.razor @@ -58,7 +58,7 @@ @code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public int planNumber { get; set; } = 0; diff --git a/MesaFabApproval.Client/Pages/Components/UserSelector.razor b/MesaFabApproval.Client/Pages/Components/UserSelector.razor index 5175bdd..02274a8 100644 --- a/MesaFabApproval.Client/Pages/Components/UserSelector.razor +++ b/MesaFabApproval.Client/Pages/Components/UserSelector.razor @@ -43,7 +43,7 @@ @code { - [CascadingParameter] MudDialogInstance MudDialog { get; set; } + [CascadingParameter] IMudDialogInstance MudDialog { get; set; } [Parameter] public User selectedUser { get; set; } diff --git a/MesaFabApproval.Client/Pages/Dashboard.razor b/MesaFabApproval.Client/Pages/Dashboard.razor index ddd5c83..2132061 100644 --- a/MesaFabApproval.Client/Pages/Dashboard.razor +++ b/MesaFabApproval.Client/Pages/Dashboard.razor @@ -26,7 +26,7 @@ - + Role @@ -47,7 +47,7 @@ @context.IssueID } - @context.SubRoleCategoryItem + @(GetRoleName(context)) @DateTimeUtilities.GetDateAsStringMinDefault(context.AssignedDate) @context.Step diff --git a/MesaFabApproval.Client/Pages/Dashboard.razor.cs b/MesaFabApproval.Client/Pages/Dashboard.razor.cs index d923cbe..42a3a93 100644 --- a/MesaFabApproval.Client/Pages/Dashboard.razor.cs +++ b/MesaFabApproval.Client/Pages/Dashboard.razor.cs @@ -118,7 +118,7 @@ public partial class Dashboard { private void GoTo(string page) { cache.Set("redirectUrl", page); - navigationManager.NavigateTo("/" + page); + navigationManager.NavigateTo(page); } private async Task GoToExternal(string url, string content) { @@ -154,4 +154,12 @@ public partial class Dashboard { if (step < 0 || step > (PCRB.Stages.Length - 1)) return string.Empty; else return PCRB.Stages[step]; } + + private string GetRoleName(Approval approval) { + string roleName = approval.SubRoleCategoryItem; + if (string.IsNullOrWhiteSpace(roleName)) { + roleName = approval.RoleName; + } + return roleName; + } } diff --git a/MesaFabApproval.Client/Pages/Login.razor b/MesaFabApproval.Client/Pages/Login.razor index df46c4d..f0b98d6 100644 --- a/MesaFabApproval.Client/Pages/Login.razor +++ b/MesaFabApproval.Client/Pages/Login.razor @@ -1,6 +1,4 @@ @page "/login" -@page "/login/{redirectUrl}" -@page "/login/{redirectUrl}/{redirectUrlSub}" @attribute [AllowAnonymous] @inject MesaFabApprovalAuthStateProvider authStateProvider @inject NavigationManager navManager @@ -46,68 +44,5 @@ } - @* - @if (processingLocal) { - - Processing - } else { - Log In (SSO) - } - *@ - -@code { - [Parameter] - public string? redirectUrl { get; set; } - [Parameter] - public string? redirectUrlSub { get; set; } - private bool success; - private bool processing = false; - private bool processingLocal = false; - private string[] errors = { }; - private string? username; - private string? password; - - private async Task SubmitLogin() { - processing = true; - if (string.IsNullOrWhiteSpace(username)) snackbar.Add("Username is required!", Severity.Error); - else if (string.IsNullOrWhiteSpace(password)) snackbar.Add("Password is required!", Severity.Error); - else { - await authStateProvider.LoginAsync(username, password); - if (!string.IsNullOrWhiteSpace(redirectUrl) && !string.IsNullOrWhiteSpace(redirectUrlSub)) { - navManager.NavigateTo($"{redirectUrl}/{redirectUrlSub}"); - } else if (!string.IsNullOrWhiteSpace(redirectUrl)) { - navManager.NavigateTo(redirectUrl); - } else { - navManager.NavigateTo("dashboard"); - } - } - processing = false; - } - - private async Task SubmitIfEnter(KeyboardEventArgs e) { - if (e.Key == "Enter" && success) { - SubmitLogin(); - } - } - - private async Task LoginLocal() { - processingLocal = true; - - await authStateProvider.LoginLocal(); - if (!string.IsNullOrWhiteSpace(redirectUrl) && !string.IsNullOrWhiteSpace(redirectUrlSub)) { - navManager.NavigateTo($"{redirectUrl}/{redirectUrlSub}"); - } else if (!string.IsNullOrWhiteSpace(redirectUrl)) { - navManager.NavigateTo(redirectUrl); - } else { - navManager.NavigateTo("dashboard"); - } - - processingLocal = false; - } -} diff --git a/MesaFabApproval.Client/Pages/Login.razor.cs b/MesaFabApproval.Client/Pages/Login.razor.cs new file mode 100644 index 0000000..00ebd3f --- /dev/null +++ b/MesaFabApproval.Client/Pages/Login.razor.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Caching.Memory; + +using MudBlazor; + +namespace MesaFabApproval.Client.Pages; + +public partial class Login { + [Inject] NavigationManager navigationManager { get; set; } + [Inject] IMemoryCache cache { get; set; } + + public string? _redirectPath { get; set; } + private bool success; + private bool processing = false; + private string[] errors = { }; + private string? username; + private string? password; + + protected override async Task OnParametersSetAsync() { + Uri uri = navigationManager.ToAbsoluteUri(navigationManager.Uri); + + if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("redirectPath", out var redirectPath)) { + _redirectPath = System.Net.WebUtility.UrlDecode(redirectPath); + } + + if (string.IsNullOrWhiteSpace(_redirectPath)) { + _redirectPath = cache.Get("redirectUrl"); + } + } + + private async Task SubmitLogin() { + processing = true; + if (string.IsNullOrWhiteSpace(username)) snackbar.Add("Username is required!", Severity.Error); + else if (string.IsNullOrWhiteSpace(password)) snackbar.Add("Password is required!", Severity.Error); + else { + await authStateProvider.LoginAsync(username, password); + if (!string.IsNullOrWhiteSpace(_redirectPath)) { + navManager.NavigateTo(_redirectPath); + } else { + navManager.NavigateTo("dashboard"); + } + } + processing = false; + } + + private async Task SubmitIfEnter(KeyboardEventArgs e) { + if (e.Key == "Enter" && success) { + SubmitLogin(); + } + } +} diff --git a/MesaFabApproval.Client/Pages/MRBAll.razor b/MesaFabApproval.Client/Pages/MRBAll.razor index 800d182..bddb3fb 100644 --- a/MesaFabApproval.Client/Pages/MRBAll.razor +++ b/MesaFabApproval.Client/Pages/MRBAll.razor @@ -1,9 +1,4 @@ @page "/mrb/all" -@using System.Globalization -@inject IMRBService mrbService -@inject ISnackbar snackbar -@inject IMemoryCache cache -@inject NavigationManager navigationManager MRB @@ -78,42 +73,3 @@ - -@code { - private bool inProcess = false; - private string searchString = ""; - private IEnumerable allMrbs = new List(); - - protected override async Task OnParametersSetAsync() { - inProcess = true; - try { - if (mrbService is null) { - throw new Exception("MRB service not injected!"); - } else { - allMrbs = await mrbService.GetAllMRBs(false); - } - } catch (Exception ex) { - snackbar.Add(ex.Message, Severity.Error); - } - inProcess = false; - } - - private bool FilterFuncForTable(MRB mrb) => FilterFunc(mrb, searchString); - - private bool FilterFunc(MRB mrb, string searchString) { - if (string.IsNullOrWhiteSpace(searchString)) - return true; - if (mrb.Title.ToLower().Contains(searchString.Trim().ToLower())) - return true; - if (mrb.OriginatorName.ToLower().Contains(searchString.Trim().ToLower())) - return true; - if (mrb.MRBNumber.ToString().Contains(searchString.Trim())) - return true; - return false; - } - - private void GoTo(string page) { - cache.Set("redirectUrl", page); - navigationManager.NavigateTo(page); - } -} diff --git a/MesaFabApproval.Client/Pages/MRBAll.razor.cs b/MesaFabApproval.Client/Pages/MRBAll.razor.cs new file mode 100644 index 0000000..8cfdb9b --- /dev/null +++ b/MesaFabApproval.Client/Pages/MRBAll.razor.cs @@ -0,0 +1,55 @@ +using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Caching.Memory; + +using MudBlazor; + +namespace MesaFabApproval.Client.Pages; + +public partial class MRBAll { + [Inject] IMRBService mrbService { get; set; } + [Inject] ISnackbar snackbar { get; set; } + [Inject] IMemoryCache cache { get; set; } + [Inject] NavigationManager navigationManager { get; set; } + + private bool inProcess = false; + private string searchString = ""; + private IEnumerable allMrbs = new List(); + + protected override async Task OnParametersSetAsync() { + inProcess = true; + try { + cache.Set("redirectUrl", "mrb/all"); + + if (mrbService is null) { + throw new Exception("MRB service not injected!"); + } else { + allMrbs = await mrbService.GetAllMRBs(false); + } + } catch (Exception ex) { + snackbar.Add(ex.Message, Severity.Error); + } + inProcess = false; + } + + private bool FilterFuncForTable(MRB mrb) => FilterFunc(mrb, searchString); + + private bool FilterFunc(MRB mrb, string searchString) { + if (string.IsNullOrWhiteSpace(searchString)) + return true; + if (mrb.Title.ToLower().Contains(searchString.Trim().ToLower())) + return true; + if (mrb.OriginatorName.ToLower().Contains(searchString.Trim().ToLower())) + return true; + if (mrb.MRBNumber.ToString().Contains(searchString.Trim())) + return true; + return false; + } + + private void GoTo(string page) { + cache.Set("redirectUrl", page); + navigationManager.NavigateTo(page); + } +} diff --git a/MesaFabApproval.Client/Pages/MRBSingle.razor.cs b/MesaFabApproval.Client/Pages/MRBSingle.razor.cs index e6c6535..763053a 100644 --- a/MesaFabApproval.Client/Pages/MRBSingle.razor.cs +++ b/MesaFabApproval.Client/Pages/MRBSingle.razor.cs @@ -65,6 +65,8 @@ public partial class MRBSingle { protected override async Task OnParametersSetAsync() { processing = true; try { + cache.Set("redirectUrl", $"mrb/{mrbNumber}"); + allActiveUsers = await userService.GetAllActiveUsers(); currentUser = authStateProvider.CurrentUser; currentUrl = navigationManager.Uri; @@ -285,7 +287,7 @@ public partial class MRBSingle { string? comments = ""; DialogParameters parameters = new DialogParameters { { x => x.comments, comments } }; - var dialog = dialogService.Show($"Approval Comments", parameters); + var dialog = await dialogService.ShowAsync($"Approval Comments", parameters); var result = await dialog.Result; @@ -412,7 +414,7 @@ public partial class MRBSingle { if (currentUser is null) { recallInProcess = false; snackbar.Add("You must be logged in to recall this MRB", Severity.Error); - navigationManager.NavigateTo($"login/mrb/{mrb.MRBNumber}"); + navigationManager.NavigateTo($"login?redirectPath=mrb/{mrb.MRBNumber}"); } else { await mrbService.RecallMRB(mrb, currentUser); mrbApprovals = await approvalService.GetApprovalsForIssueId(mrb.MRBNumber, true); diff --git a/MesaFabApproval.Client/Pages/PCRBAll.razor b/MesaFabApproval.Client/Pages/PCRBAll.razor index d01da3e..fbf631e 100644 --- a/MesaFabApproval.Client/Pages/PCRBAll.razor +++ b/MesaFabApproval.Client/Pages/PCRBAll.razor @@ -1,9 +1,4 @@ @page "/pcrb/all" -@using System.Globalization -@inject IPCRBService pcrbService -@inject ISnackbar snackbar -@inject IMemoryCache cache -@inject NavigationManager navigationManager PCRB @@ -25,6 +20,7 @@ Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" + Immediate Class="mt-0" /> @@ -43,19 +39,37 @@ Owner - Stage + + + Type + + + + + Stage + + Submitted Date - - Last Updated + + Completed Date - + + Follow Up Date + + + + Closed Date @@ -66,10 +80,20 @@ @context.Title @context.OwnerName + @context.Type @(GetStageName(context.CurrentStep)) @DateTimeUtilities.GetDateAsStringMinDefault(context.InsertTimeStamp) - @DateTimeUtilities.GetDateAsStringMinDefault(context.LastUpdateDate) - @DateTimeUtilities.GetDateAsStringMaxDefault(context.ClosedDate) + @DateTimeUtilities.GetDateAsStringMaxDefault(context.ClosedDate) + @(DateTimeUtilities.GetDateAsStringMaxDefault(context.FollowUps + .FirstOrDefault()?.FollowUpDate + ) + ) + + @(DateTimeUtilities.GetDateAsStringMaxDefault(context.FollowUps + .FirstOrDefault()?.CompletedDate + ) + ) + @@ -80,47 +104,3 @@ - -@code { - private bool inProcess = false; - private string searchString = ""; - private IEnumerable allPCRBs = new List(); - - protected override async Task OnParametersSetAsync() { - inProcess = true; - try { - if (pcrbService is null) { - throw new Exception("PCRB service not injected!"); - } else { - allPCRBs = await pcrbService.GetAllPCRBs(false); - } - } catch (Exception ex) { - snackbar.Add(ex.Message, Severity.Error); - } - inProcess = false; - } - - private bool FilterFuncForTable(PCRB pcrb) => FilterFunc(pcrb, searchString); - - private bool FilterFunc(PCRB pcrb, string searchString) { - if (string.IsNullOrWhiteSpace(searchString)) - return true; - if (pcrb.Title.ToLower().Contains(searchString.Trim().ToLower())) - return true; - if (pcrb.OwnerName.ToLower().Contains(searchString.Trim().ToLower())) - return true; - if (pcrb.PlanNumber.ToString().Contains(searchString.Trim())) - return true; - return false; - } - - private void GoTo(string page) { - cache.Set("redirectUrl", page); - navigationManager.NavigateTo(page); - } - - private string GetStageName(int step) { - if (step >= PCRB.Stages.Length || step < 0) return ""; - return PCRB.Stages[step]; - } -} diff --git a/MesaFabApproval.Client/Pages/PCRBAll.razor.cs b/MesaFabApproval.Client/Pages/PCRBAll.razor.cs new file mode 100644 index 0000000..6c0fb62 --- /dev/null +++ b/MesaFabApproval.Client/Pages/PCRBAll.razor.cs @@ -0,0 +1,64 @@ +using MesaFabApproval.Client.Services; +using MesaFabApproval.Shared.Models; + +using Microsoft.AspNetCore.Components; +using Microsoft.Extensions.Caching.Memory; + +using MudBlazor; + +namespace MesaFabApproval.Client.Pages; + +public partial class PCRBAll { + [Inject] IPCRBService pcrbService { get; set; } + [Inject] ISnackbar snackbar { get; set; } + [Inject] IMemoryCache cache { get; set; } + [Inject] NavigationManager navigationManager { get; set; } + + private bool inProcess = false; + private string searchString = ""; + private IEnumerable allPCRBs = new List(); + + protected override async Task OnParametersSetAsync() { + inProcess = true; + try { + cache.Set("redirectUrl", "pcrb/all"); + + if (pcrbService is null) { + throw new Exception("PCRB service not injected!"); + } else { + allPCRBs = await pcrbService.GetAllPCRBs(false); + } + } catch (Exception ex) { + snackbar.Add(ex.Message, Severity.Error); + } + inProcess = false; + } + + private bool FilterFuncForTable(PCRB pcrb) => FilterFunc(pcrb, searchString); + + private bool FilterFunc(PCRB pcrb, string searchString) { + if (string.IsNullOrWhiteSpace(searchString)) + return true; + if (pcrb.Title.ToLower().Contains(searchString.Trim().ToLower())) + return true; + if (pcrb.OwnerName.ToLower().Contains(searchString.Trim().ToLower())) + return true; + if (pcrb.Type.ToLower().Contains(searchString.Trim().ToLower())) + return true; + if (GetStageName(pcrb.CurrentStep).ToLower().Contains(searchString.Trim().ToLower())) + return true; + if (pcrb.PlanNumber.ToString().Contains(searchString.Trim())) + return true; + return false; + } + + private void GoTo(string page) { + cache.Set("redirectUrl", page); + navigationManager.NavigateTo(page); + } + + private string GetStageName(int step) { + if (step >= PCRB.Stages.Length || step < 0) return ""; + return PCRB.Stages[step]; + } +} diff --git a/MesaFabApproval.Client/Pages/PCRBSingle.razor b/MesaFabApproval.Client/Pages/PCRBSingle.razor index eef2240..c9cc754 100644 --- a/MesaFabApproval.Client/Pages/PCRBSingle.razor +++ b/MesaFabApproval.Client/Pages/PCRBSingle.razor @@ -6,20 +6,22 @@ + Variant="Variant.Outlined" + Color="Color.Dark" + OnClick="@ReturnToAllPcrbs" + Size="Size.Large" /> PCRB @planNumber @if (pcrb is not null) { + TimelinePosition="TimelinePosition.Bottom"> @for (int i = 0; i < PCRB.Stages.Length; i++) { Color color; - if (pcrb.CurrentStep > i || pcrb.CurrentStep == (PCRB.Stages.Length - 1)) { + if (pcrb.CurrentStep > i || + (i == (int)PCRB.StagesEnum.Complete && pcrb.CurrentStep == (int)PCRB.StagesEnum.Complete) || + (i == (int)PCRB.StagesEnum.Closed && pcrb.CurrentStep == (int)PCRB.StagesEnum.Closed)) { color = Color.Success; } else if (pcrb.CurrentStep == i) { color = Color.Info; @@ -35,14 +37,14 @@ } - bool pcrbIsSubmitted = pcrb.CurrentStep > 0 && pcrb.InsertTimeStamp > DateTimeUtilities.MIN_DT; - bool pcrbIsComplete = pcrb.ClosedDate < DateTimeUtilities.MAX_DT && pcrb.CurrentStep == (PCRB.Stages.Length - 1); + bool pcrbIsSubmitted = pcrb.CurrentStep > (int)PCRB.StagesEnum.Draft && pcrb.InsertTimeStamp > DateTimeUtilities.MIN_DT; + bool pcrbIsComplete = pcrb.ClosedDate < DateTimeUtilities.MAX_DT && pcrb.CurrentStep >= (int)PCRB.StagesEnum.Complete; bool userIsOriginator = pcrb.OwnerID == authStateProvider.CurrentUser?.UserID; bool userIsAdmin = authStateProvider.CurrentUser is null ? false : authStateProvider.CurrentUser.IsAdmin; bool userIsApprover = UserIsApprover(); @if ((!pcrbIsSubmitted && !string.IsNullOrWhiteSpace(pcrb.Title) && (userIsOriginator || userIsAdmin)) || - (!pcrbIsSubmitted && pcrb.PlanNumber > 0 && (userIsOriginator || userIsAdmin))) { + (!pcrbIsSubmitted && pcrb.PlanNumber > 0 && (userIsOriginator || userIsAdmin))) { @@ -61,9 +63,9 @@ } @if (!pcrbIsSubmitted && pcrb.PlanNumber > 0 && (userIsOriginator || userIsAdmin)) { + Color="Color.Secondary" + Disabled="@deleteInProcess" + OnClick=DeletePCRB> @if (deleteInProcess) { Processing @@ -91,114 +93,114 @@ } + Class="p-2 m-2 d-flex flex-wrap gap-3 justify-content-center align-content-start" + Elevation="10"> + Text="@pcrb.PlanNumber.ToString()" + T="int" + Disabled="true" + Label="Change#" + Required + Variant="Variant.Outlined" /> + Text="@pcrb.Title" + Disabled="@(pcrbIsSubmitted)" + T="string" + AutoGrow + AutoFocus + Immediate + Clearable + Required + Variant="Variant.Outlined" + Label="Project Name" /> + Label="Originator" + Variant="Variant.Outlined" + Required + Clearable + AnchorOrigin="Origin.BottomCenter" + ToStringFunc="@UserToNameConverter" + Disabled=@(pcrbIsSubmitted) + @bind-Value=@selectedOwner> @foreach (User user in allActiveUsers.OrderBy(u => u.LastName)) { } + Variant="Variant.Outlined" + Required + Clearable + AnchorOrigin="Origin.BottomCenter" + Disabled="@pcrbIsSubmitted" + @bind-Value="@pcrb.ChangeLevel" + Text="@pcrb.ChangeLevel" + Label="Change Level"> + Variant="Variant.Outlined" + Required + Clearable + AnchorOrigin="Origin.BottomCenter" + Disabled="@pcrbIsSubmitted" + @bind-Value="@pcrb.Type" + Text="@pcrb.Type" + Label="Change Type"> + Color="Color.Tertiary" + @bind-Value=pcrb.IsITAR + Label="Export Controlled" + LabelPosition="LabelPosition.Start" /> + T="string" + Value="@DateTimeUtilities.GetDateAsStringMinDefault(pcrb.InsertTimeStamp)" + Label="Submit Date" + Variant="Variant.Outlined" /> + T="string" + Value="@DateTimeUtilities.GetDateAsStringMinDefault(pcrb.LastUpdateDate)" + Label="Last Update" + Variant="Variant.Outlined" /> + T="string" + Value="@DateTimeUtilities.GetDateAsStringMaxDefault(pcrb.ClosedDate)" + Label="Complete Date" + Variant="Variant.Outlined" /> + Class="p-2 m-2 d-flex flex-wrap gap-3 justify-content-center align-content-start" + Elevation="10"> + Text="@pcrb.ChangeDescription" + Disabled="@(pcrbIsSubmitted)" + T="string" + AutoGrow + Immediate + Clearable + Required + Variant="Variant.Outlined" + Label="Description Of Change" /> + Text="@pcrb.ReasonForChange" + Disabled="@(pcrbIsSubmitted)" + T="string" + AutoGrow + Immediate + Clearable + Required + Variant="Variant.Outlined" + Label="Reason For Change" /> - @if (pcrb.PlanNumber > 0 && pcrb.CurrentStep > 0) { + @if (pcrb.PlanNumber > 0 && pcrb.CurrentStep > (int)PCRB.StagesEnum.Draft) { @for (int i = 1; i < 4; i++) { int current_i = i; @@ -229,7 +231,7 @@ int currentStagePendingApprovalsCount = currentStageApprovals.Where(a => a.ItemStatus == 0 && a.AssignedDate > DateTimeUtilities.MIN_DT).Count(); int currentStageApprovedApprovalsCount = currentStageApprovals.Where(a => a.ItemStatus == 1).Count(); int currentStageDeniedApprovalsCount = currentStageApprovals.Where(a => a.ItemStatus == -1).Count(); - + bool currentStageApproved = currentStageApprovedApprovalsCount >= 4 && currentStageUnsubmittedApprovalCount == 0 && currentStagePendingApprovalsCount == 0; bool currentStageSubmitted = (currentStageApprovals.Count() > 0 && currentStagePendingApprovalsCount > 0 && @@ -260,7 +262,7 @@ @($"PCR {current_i}") @if (previousStageSubmitted && (attachmentsMissing || actionItemsIncomplete || - affectedDocumentsIncomplete || approvalsIncomplete)) { + affectedDocumentsIncomplete || approvalsIncomplete)) { StringBuilder sb = new(); int missingSectionCount = 0; sb.Append("Incomplete sections: "); @@ -299,31 +301,31 @@ - + Class="p-2 m-2 d-flex flex-column justify-start"> + Supporting Documents - + + Class="m-2" + Striped="true" + SortLabel="Sort By" + Hover="true"> @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && previousStageSubmitted && !currentStageSubmitted) { @if (current_i == 1) { + Color="Color.Tertiary" + Href="https://plm.intra.infineon.com/Windchill/netmarkets/jsp/ext/infineon/dcoidreleased.jsp?obid=OR:wt.doc.WTDocument:1477717325" + Target="_blank"> Download PCRB Template } + Color="Color.Tertiary" + OnClick="@((e) => UploadAttachment(current_i))" + Disabled="@attachmentUploadInProcess" + StartIcon="@Icons.Material.Filled.AttachFile"> @if (attachmentUploadInProcess) { Processing @@ -354,8 +356,8 @@ + download="@(context.FileName)" + target="_top"> @context.FileName @@ -364,9 +366,9 @@ @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && previousStageSubmitted && !currentStageSubmitted) { + Variant="Variant.Filled" + Disabled="@deleteAttachmentInProcess" + OnClick="@((e) => DeleteAttachment(context))"> @if (deleteAttachmentInProcess) { Deleting @@ -389,24 +391,24 @@ } + Class="m-2" + Striped="true" + SortLabel="Sort By" + Hover="true"> @if (previousStageSubmitted && !currentStageSubmitted) { + Color="Color.Tertiary" + OnClick="@((e) => CreateNewActionItem(current_i))"> New Action Item } @if (currentStagePendingActionItemCount > 0) { + Color="Color.Tertiary" + Disabled="@processing" + OnClick="@((e) => CloseAllActionItems(current_i))"> Complete All Actions } @@ -448,14 +450,14 @@ @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && context.ClosedByID == 0) { + Variant="Variant.Filled" + OnClick="@((e) => UpdateActionItem(context))"> Update @if (!currentStageSubmitted) { + Variant="Variant.Filled" + OnClick="@((e) => DeleteActionItem(context))"> Delete } @@ -468,10 +470,10 @@ Affected Documents + Class="m-2" + Striped="true" + SortLabel="Sort By" + Hover="true"> @@ -513,8 +515,8 @@ context.GetEcnNumberString(); } else { + Href=@($"{config["OldFabApprovalUrl"]}/ECN/Edit?IssueID={context.GetEcnNumberString()}") + Target="_blank"> @context.GetEcnNumberString() } @@ -526,8 +528,8 @@ @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && !currentStageSubmitted) { + Variant="Variant.Filled" + OnClick="@((e) => UpdatePCR3Document(context))"> Update @@ -539,23 +541,23 @@ Attendees + Class="m-2" + Striped="true" + SortLabel="Sort By" + Hover="true"> @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && !currentStageSubmitted) { + Variant="Variant.Filled" + Class="m-1" + OnClick="@((e) => AddAttendee(current_i))"> Add Attendee + Variant="Variant.Filled" + Class="m-1" + OnClick="@((e) => MarkAllAttended(current_i))"> Mark All Attended @@ -577,9 +579,9 @@ + Icon="@(context.Attended ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank)" + Color="Color.Tertiary" + OnClick="@((e) => MarkAttended(context))" /> @@ -592,33 +594,33 @@ } + Class="m-2" + Striped="true" + SortLabel="Sort By" + Hover="true"> @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && previousStageApproved) { @if (!currentStageSubmitted) { + Color="Color.Tertiary" + OnClick="@((e) => AddApprover(current_i))"> Add Approver } @if (previousStageSubmitted && !currentStageSubmitted && currentStageAttachments.Count() > 0 && - !affectedDocumentsIncomplete && allActionItemsComplete && - !previousStageHasOpenGatedActionItems && atLeastOneAttendeeAttended) { + !affectedDocumentsIncomplete && allActionItemsComplete && + !previousStageHasOpenGatedActionItems && atLeastOneAttendeeAttended) { @if (submitInProcess) { Submitting } else { - Submit For Approval - + Color="Color.Tertiary" + Disabled="@submitInProcess" + OnClick="@((e) => SubmitForApproval(current_i))"> + Submit For Approval + } } @@ -652,31 +654,31 @@ @DateTimeUtilities.GetDateAsStringMaxDefault(context.CompletedDate) @context.Comments @if (pcrb.ClosedDate >= DateTimeUtilities.MAX_DT && (currentStageUnsubmittedApprovalCount > 0 || - currentStagePendingApprovalsCount > 0)) { + currentStagePendingApprovalsCount > 0)) { @if (context.ItemStatus == 0 && userIsAdmin) { + Variant="Variant.Filled" + Class="m-1" + OnClick="@((e) => UpdateApproval(context))"> Update } @if ((current_i < 3 || pcr3Documents.Where(d=>d.CompletedByID == 0).Count() == 0) && - (authStateProvider.CurrentUser is not null && context.UserID == authStateProvider.CurrentUser.UserID) && - context.ItemStatus == 0 && context.AssignedDate > DateTimeUtilities.MIN_DT) { + (authStateProvider.CurrentUser is not null && context.UserID == authStateProvider.CurrentUser.UserID) && + context.ItemStatus == 0 && context.AssignedDate > DateTimeUtilities.MIN_DT) { + Variant="Variant.Filled" + Class="m-1" + Disabled="@processing" + OnClick="@((e) => ApprovePCR(current_i))"> Approve + Variant="Variant.Filled" + Class="m-1" + Disabled="@processing" + OnClick="@((e) => DenyPCR(current_i))"> Deny } @@ -688,6 +690,195 @@ } + + @if (pcrb.FollowUps.Count() > 0) { + + + Follow Up + + + + + Supporting Documents + + + + @if (!pcrb.FollowUps.First().IsComplete && !pcrb.FollowUps.First().IsPendingApproval) { + + @if (attachmentUploadInProcess) { + + Processing + } + else { + Upload Document + } + + } + @if (!pcrb.FollowUps.First().IsPendingApproval && !pcrb.FollowUps.First().IsComplete && + attachments.Where(a => a.Step == 5).Count() > 0 && + approvals.Where(a => a.Step == 5 && + a.UserID == authStateProvider.CurrentUser.UserID && + a.ItemStatus == 0).Count() > 0) { + + @if (followUpSubmitInProcess) { + + Processing + } + else { + Submit For Closure + } + + } + @if (pcrb.FollowUps.First().IsPendingApproval && !pcrb.FollowUps.First().IsComplete && + attachments.Where(a => a.Step == 5).Count() > 0 && + approvals.Where(a => a.Step == 5 && + a.UserID == authStateProvider.CurrentUser.UserID).Count() > 0) + { + @if (userIsQA) { + + @if (followUpApproveInProcess) { + + Processing + } else { + Close + } + + + @if (followUpDenyInProcess) { + + Processing + } else { + Reject + } + + } else + { + + @if (followUpDenyInProcess) { + + Processing + } else { + Recall + } + + } + } + + + + + + File Name + + + + + Uploaded By + + + + + Uploaded Date + + + + + + + @context.FileName + + + @(context.UploadedBy is null ? string.Empty : context.UploadedBy.GetFullName()) + @context.UploadDateTime.ToString("yyyy-MM-dd HH:mm") + @if (!pcrb.FollowUps.First().IsComplete && !pcrb.FollowUps.First().IsPendingApproval) + { + + + @if (deleteAttachmentInProcess) + { + + Deleting + } + else + { + Delete + } + + + } + + + + + Follow Up Date + + + @if (followUpComments.Count() > 0) { + + Revision Comments + + + Date + User + Comment + + + @context.CommentDate.ToString("MM/dd/yyyy hh:mm") + @(context.User?.GetFullName() ?? string.Empty) + @context.Comment + + + + } + + + + } } } diff --git a/MesaFabApproval.Client/Pages/PCRBSingle.razor.cs b/MesaFabApproval.Client/Pages/PCRBSingle.razor.cs index 55d3f02..b1834f7 100644 --- a/MesaFabApproval.Client/Pages/PCRBSingle.razor.cs +++ b/MesaFabApproval.Client/Pages/PCRBSingle.razor.cs @@ -34,36 +34,39 @@ public partial class PCRBSingle { private IEnumerable attachments = new List(); private IEnumerable actionItems = new List(); private IEnumerable pcr3Documents = new List(); + private IEnumerable followUpComments = new List(); + + private DateTime? followUpDate; private IEnumerable qualityApproverUserIds = new List(); private IEnumerable allActiveUsers = new List(); private User selectedOwner = null; + private bool userIsQA = false; + private bool processing = false; private bool saveInProcess = false; private bool deleteInProcess = false; private bool submitInProcess = false; - private bool approvalInProcess = false; - private bool denialInProcess = false; - private bool recallInProcess = false; private bool attachmentUploadInProcess = false; - private bool updateAttachmentInProcess = false; private bool deleteAttachmentInProcess = false; - private bool addActionItemInProcess = false; - - private string attachmentSearchString = ""; - - private string actionItemSearchString = ""; + private bool followUpSubmitInProcess = false; + private bool followUpApproveInProcess = false; + private bool followUpDenyInProcess = false; protected override async Task OnParametersSetAsync() { processing = true; try { + cache.Set("redirectUrl", $"pcrb/{planNumber}"); + allActiveUsers = await userService.GetAllActiveUsers(); if (qualityApproverUserIds.Count() == 0) qualityApproverUserIds = await GetQualityApproverUserIds(); + userIsQA = qualityApproverUserIds.Contains(authStateProvider.CurrentUser?.UserID ?? -1); + if (!string.IsNullOrWhiteSpace(planNumber) && Int32.TryParse(planNumber, out planNumberInt)) { pcrb = await pcrbService.GetPCRBByPlanNumber(planNumberInt, false); approvals = await approvalService.GetApprovalsForIssueId(planNumberInt, false); @@ -71,6 +74,10 @@ public partial class PCRBSingle { attachments = await pcrbService.GetAttachmentsByPlanNumber(planNumberInt, false); actionItems = await pcrbService.GetActionItemsForPlanNumber(planNumberInt, false); pcr3Documents = await pcrbService.GetPCR3DocumentsForPlanNumber(planNumberInt, false); + followUpComments = await pcrbService.GetFollowUpCommentsByPlanNumber(planNumberInt, false); + + if (followUpDate is null) + followUpDate = pcrb.FollowUps.Count() > 0 ? pcrb.FollowUps.First().FollowUpDate : DateTimeUtilities.MAX_DT; List createPCR3DocumentTasks = new(); if (pcr3Documents.Count() <= 0) { @@ -117,9 +124,9 @@ public partial class PCRBSingle { if (pcrb.OwnerID > 0) selectedOwner = await userService.GetUserByUserId(pcrb.OwnerID); - if (pcrb.CurrentStep > 0 && pcrb.CurrentStep < 4) { + if (pcrb.CurrentStep > (int)PCRB.StagesEnum.Draft && pcrb.CurrentStep < (int)PCRB.StagesEnum.Complete) { bool stageHasAdvanced = false; - for (int stage = pcrb.CurrentStep; stage < 4; stage++) { + for (int stage = pcrb.CurrentStep; stage < (int)PCRB.StagesEnum.Complete; stage++) { int current_stage = stage; if (pcrb.CurrentStep == current_stage) { IEnumerable currentStageApprovals = approvals.Where(a => a.Step == current_stage); @@ -128,7 +135,7 @@ public partial class PCRBSingle { bool currentStageApproved = currentStageApprovedApprovalsCount >= 3 && currentStagePendingApprovalsCount == 0; if (currentStageApproved) { - if (pcrb.CurrentStep == 3) { + if (pcrb.CurrentStep == (int)PCRB.StagesEnum.PCR3) { int openActionItemCount = actionItems.Where(a => a.ClosedByID == 0).Count(); int openAffectedDocumentsCount = pcr3Documents.Where(d => d.CompletedByID == 0).Count(); @@ -145,7 +152,7 @@ public partial class PCRBSingle { } if (stageHasAdvanced) { - if (pcrb.CurrentStep == 4) { + if (pcrb.CurrentStep == (int)PCRB.StagesEnum.Complete) { pcrb.ClosedDate = DateTime.Now; string message = $"PCRB# {pcrb.PlanNumber} - {pcrb.Title} is complete"; @@ -165,6 +172,34 @@ public partial class PCRBSingle { await OnParametersSetAsync(); } } + + if (pcrb.CurrentStep == (int)PCRB.StagesEnum.Complete && pcrb.FollowUps.Count() == 0) { + PCRBFollowUp followUp = new() { + PlanNumber = pcrb.PlanNumber, + Step = (int)PCRB.StagesEnum.FollowUp, + FollowUpDate = pcrb.ClosedDate.AddMonths(6) + }; + + await pcrbService.CreateFollowUp(followUp); + + pcrb = await pcrbService.GetPCRBByPlanNumber(pcrb.PlanNumber, true); + + if (pcrb.FollowUps.Count() == 0) + throw new Exception("unable to create follow up"); + + StateHasChanged(); + await OnParametersSetAsync(); + } + + if (pcrb.CurrentStep == (int)PCRB.StagesEnum.Complete && pcrb.FollowUps.Count() > 0 && + DateTime.Now >= pcrb.FollowUps.First().FollowUpDate.AddDays(-15)) { + pcrb.CurrentStep = (int)PCRB.StagesEnum.FollowUp; + await pcrbService.UpdatePCRB(pcrb); + pcrb = await pcrbService.GetPCRBByPlanNumber(pcrb.PlanNumber, true); + + StateHasChanged(); + await OnParametersSetAsync(); + } } else { int ownerID = 0; string ownerName = string.Empty; @@ -177,7 +212,7 @@ public partial class PCRBSingle { pcrb = new() { OwnerID = ownerID, OwnerName = ownerName, - CurrentStep = 0 + CurrentStep = (int)PCRB.StagesEnum.Draft }; } @@ -215,7 +250,7 @@ public partial class PCRBSingle { private bool PCRBReadyToSubmit(int step) { bool readyToSubmit = GetIncompleteFields().Count() <= 0; - readyToSubmit = readyToSubmit && pcrb.CurrentStep > 0; + readyToSubmit = readyToSubmit && pcrb.CurrentStep > (int)PCRB.StagesEnum.Draft; readyToSubmit = readyToSubmit && attachments.Where(a => a.Step == step).Count() > 0; @@ -241,7 +276,7 @@ public partial class PCRBSingle { pcrb.OwnerID = selectedOwner.UserID; pcrb.OwnerName = selectedOwner.GetFullName(); - if (pcrb.CurrentStep == 0 && GetIncompleteFields().Count() <= 0) + if (pcrb.CurrentStep == (int)PCRB.StagesEnum.Draft && GetIncompleteFields().Count() <= 0) pcrb.CurrentStep++; if (initialPlanNumber <= 0) { @@ -300,34 +335,54 @@ public partial class PCRBSingle { } private async Task> GetQualityApproverUserIds() { - List qualityApproverUserIds = new(); - try { - int roleId = await approvalService.GetRoleIdForRoleName("Module Manager"); + HashSet? qualityApproverUserIds = cache.Get>("qualityApproverUserIds"); - if (roleId <= 0) throw new Exception($"could not find Module Manager role ID"); + if (qualityApproverUserIds is null || qualityApproverUserIds.Count() == 0) { + qualityApproverUserIds = new(); - IEnumerable subRoles = await approvalService.GetSubRolesForSubRoleName("MMSubRole", roleId); + int roleId = await approvalService.GetRoleIdForRoleName("Module Manager"); - foreach (SubRole subRole in subRoles) { - if (subRole.SubRoleCategoryItem.Equals("Quality", StringComparison.InvariantCultureIgnoreCase)) { + if (roleId <= 0) throw new Exception($"could not find Module Manager role ID"); + + IEnumerable subRoles = await approvalService.GetSubRolesForSubRoleName("MMSubRole", roleId); + + foreach (SubRole subRole in subRoles) { + if (subRole.SubRoleCategoryItem.Equals("Quality", StringComparison.InvariantCultureIgnoreCase)) { + IEnumerable subRoleMembers = await approvalService.GetApprovalGroupMembers(subRole.SubRoleID); + foreach (User user in subRoleMembers) qualityApproverUserIds.Add(user.UserID); + } + } + + string roleName = "QA_PRE_APPROVAL"; + string subRoleName = "QA_PRE_APPROVAL"; + + roleId = await approvalService.GetRoleIdForRoleName(roleName); + + if (roleId <= 0) throw new Exception($"could not find {roleName} role ID"); + + subRoles = await approvalService.GetSubRolesForSubRoleName(subRoleName, roleId); + + foreach (SubRole subRole in subRoles) { IEnumerable subRoleMembers = await approvalService.GetApprovalGroupMembers(subRole.SubRoleID); foreach (User user in subRoleMembers) qualityApproverUserIds.Add(user.UserID); } - } - string roleName = "QA_PRE_APPROVAL"; - string subRoleName = "QA_PRE_APPROVAL"; + roleName = "QA_FINAL_APPROVAL"; + subRoleName = "QA_FINAL_APPROVAL"; - roleId = await approvalService.GetRoleIdForRoleName(roleName); + roleId = await approvalService.GetRoleIdForRoleName(roleName); - if (roleId <= 0) throw new Exception($"could not find {roleName} role ID"); + if (roleId <= 0) throw new Exception($"could not find {roleName} role ID"); - subRoles = await approvalService.GetSubRolesForSubRoleName(subRoleName, roleId); + subRoles = await approvalService.GetSubRolesForSubRoleName(subRoleName, roleId); - foreach (SubRole subRole in subRoles) { - IEnumerable subRoleMembers = await approvalService.GetApprovalGroupMembers(subRole.SubRoleID); - foreach (User user in subRoleMembers) qualityApproverUserIds.Add(user.UserID); + foreach (SubRole subRole in subRoles) { + IEnumerable subRoleMembers = await approvalService.GetApprovalGroupMembers(subRole.SubRoleID); + foreach (User user in subRoleMembers) qualityApproverUserIds.Add(user.UserID); + } + + cache.Set("qualityApproverUserIds", qualityApproverUserIds); } return qualityApproverUserIds; @@ -445,9 +500,7 @@ public partial class PCRBSingle { Approval? latestQaPreApproval = currentStageApprovals .Where(a => a.SubRoleCategoryItem.Equals("QA Pre Approver")) .OrderByDescending(a => a.AssignedDate) - .FirstOrDefault(); - - if (latestQaPreApproval is null) throw new Exception("QA pre approval not found"); + .FirstOrDefault() ?? throw new Exception("QA pre approval not found"); bool qaPreApprovalDenied = latestQaPreApproval.ItemStatus == -1; if (qaPreApprovalDenied && currentStageUnsubmittedApprovalCount >= 3) { @@ -493,9 +546,8 @@ public partial class PCRBSingle { await Task.WhenAll(createCopiedApprovalsTasks); } else { Approval? unassignedQaPreApproval = currentStageApprovals.Where(a => a.SubRoleCategoryItem.Equals("QA Pre Approver") && - a.AssignedDate <= DateTimeUtilities.MIN_DT).FirstOrDefault(); - - if (unassignedQaPreApproval is null) throw new Exception("unassigned QA pre approval not found"); + a.AssignedDate <= DateTimeUtilities.MIN_DT).FirstOrDefault() ?? + throw new Exception("unassigned QA pre approval not found"); unassignedQaPreApproval.AssignedDate = DateTime.Now; await approvalService.UpdateApproval(unassignedQaPreApproval); @@ -505,7 +557,7 @@ public partial class PCRBSingle { await pcrbService.NotifyNewApprovals(pcrb); - if (pcrb.CurrentStep == 1) { + if (pcrb.CurrentStep == (int)PCRB.StagesEnum.PCR1) { pcrb.InsertTimeStamp = DateTime.Now; await pcrbService.UpdatePCRB(pcrb); @@ -525,23 +577,6 @@ public partial class PCRBSingle { } } - private async Task SetUserForApproval(Approval approval, User user) { - if (approval is null) throw new ArgumentNullException("approval cannot be null"); - if (user is null) throw new ArgumentNullException("user cannot be null"); - - if (approval.CompletedDate < DateTimeUtilities.MAX_DT || approval.ItemStatus != 0) - throw new ArgumentException("cannot reassign a complete approval"); - - approval.UserID = user.UserID; - approval.User = user; - approval.NotifyDate = DateTimeUtilities.MIN_DT; - - await approvalService.UpdateApproval(approval); - await approvalService.GetApprovalsForIssueId(approval.IssueID, true); - - await pcrbService.NotifyNewApprovals(pcrb); - } - private async Task ApprovePCR(int step) { if (!processing) { try { @@ -1136,4 +1171,328 @@ public partial class PCRBSingle { if (itemStatus > 0) return "Approved"; return "Pending"; } + + private async Task UpdateFollowUpDate(DateTime? newFollowUpDate) { + if (followUpDate is not null || followUpDate <= DateTimeUtilities.MAX_DT) { + try { + if (newFollowUpDate is null) + throw new Exception("follow up date cannot be null"); + + if (authStateProvider.CurrentUser is null) { + snackbar.Add("You must log in to change the follow up date", Severity.Error); + await authStateProvider.Logout(); + return; + } + + DateTime oldFollowUpDate = pcrb.FollowUps.First().FollowUpDate; + + followUpDate = newFollowUpDate; + pcrb.FollowUps.First().FollowUpDate = (DateTime)newFollowUpDate; + await pcrbService.UpdateFollowUp(pcrb.FollowUps.First()); + + pcrb = await pcrbService.GetPCRBByPlanNumber(pcrb.PlanNumber, true); + + string comments = ""; + + DialogParameters parameters = new DialogParameters { { x => x.comments, comments } }; + var dialog = await dialogService.ShowAsync($"Follow Up Date Change Comment", parameters); + + DialogResult? result = await dialog.Result; + + if (result is null || result.Canceled || result.Data is null || string.IsNullOrWhiteSpace(result.Data?.ToString())) { + followUpDate = oldFollowUpDate; + pcrb.FollowUps.First().FollowUpDate = oldFollowUpDate; + await pcrbService.UpdateFollowUp(pcrb.FollowUps.First()); + + pcrb = await pcrbService.GetPCRBByPlanNumber(pcrb.PlanNumber, true); + + throw new Exception("you must provide a comment"); + } + + comments = result.Data?.ToString() ?? string.Empty; + + comments = comments.Trim(); + + StringBuilder commentBuilder = new(); + + commentBuilder.Append($"Changing follow up date from {oldFollowUpDate.ToString("MM/dd/yyyy")} "); + commentBuilder.Append($"to {pcrb.FollowUps.First().FollowUpDate.ToString("MM/dd/yyyy")}. Comments: {comments}"); + + PCRBFollowUpComment comment = new() { + PlanNumber = pcrb.FollowUps.First().PlanNumber, + FollowUpID = pcrb.FollowUps.First().ID, + Comment = commentBuilder.ToString(), + UserID = authStateProvider.CurrentUser.UserID + }; + + await pcrbService.CreateFollowUpComment(comment); + + DateTime fifteenDaysFromNow = DateTime.Now.AddDays(15); + + if (pcrb.FollowUps.First().FollowUpDate > fifteenDaysFromNow) { + IEnumerable step5Approvals = approvals.Where(a => a.Step == 5 && a.ItemStatus == 0); + foreach (Approval approval in step5Approvals) { + await approvalService.DeleteApproval(approval.ApprovalID); + await approvalService.GetApprovalsForUserId(approval.UserID, true); + } + + approvals = await approvalService.GetApprovalsForIssueId(pcrb.PlanNumber, true); + } else if (approvals.Where(a => a.Step == 5 && a.ItemStatus == 0).Count() == 0) { + Approval newApproval = new Approval { + IssueID = pcrb.PlanNumber, + RoleName = "PCRB Owner Follow Up", + SubRole = "PCRBOwnerFollowUp", + SubRoleCategoryItem = "PCRB Owner Follow Up", + UserID = pcrb.OwnerID, + SubRoleID = 999, + AssignedDate = DateTime.Now, + TaskID = pcrb.FollowUps.First().ID, + Step = 5, + NotifyDate = DateTime.Now + }; + + await approvalService.CreateApproval(newApproval); + } + + commentBuilder.Clear(); + commentBuilder.Append($"Effectiveness review date for PCRB# {pcrb.PlanNumber} - {pcrb.Title} has been changed to "); + commentBuilder.Append($"{pcrb.FollowUps.First().FollowUpDate.ToString("MM/dd/yyyy")}. "); + + PCRBNotification notification = new() { + Message = commentBuilder.ToString(), + Subject = $"[PCRB Effectiveness Review Date Change] {pcrb.PlanNumber} - {pcrb.Title}", + PCRB = pcrb, + NotifyQaPreApprover = true + }; + + await pcrbService.NotifyOriginator(notification); + } catch (Exception ex) { + snackbar.Add($"Unable to update follow up date, because {ex.Message}", Severity.Error); + } + + StateHasChanged(); + await OnParametersSetAsync(); + } + } + + private async Task SubmitFollowUpForApproval() { + if (!followUpSubmitInProcess) { + try { + followUpSubmitInProcess = true; + + if (pcrb.FollowUps.Count() > 0) { + PCRBFollowUp followUp = pcrb.FollowUps.First(); + followUp.IsPendingApproval = true; + await pcrbService.UpdateFollowUp(followUp); + + List allSubRoles = new(); + + int roleId = await approvalService.GetRoleIdForRoleName("Module Manager"); + + if (roleId <= 0) throw new Exception($"could not find Module Manager role ID"); + + List qualityMMSubRoles = (await approvalService.GetSubRolesForSubRoleName("MMSubRole", roleId)).ToList(); + + foreach (SubRole subRole in qualityMMSubRoles) { + if (subRole.SubRoleCategoryItem.Equals("Quality")) + allSubRoles.Add(subRole); + } + + roleId = await approvalService.GetRoleIdForRoleName("QA_FINAL_APPROVAL"); + + if (roleId <= 0) throw new Exception($"could not find QA Final Approval role ID"); + + IEnumerable qaFinalApprovalSubRoles = + (await approvalService.GetSubRolesForSubRoleName("QA_FINAL_APPROVAL", roleId)).ToList(); + + foreach (SubRole subRole in qaFinalApprovalSubRoles) + allSubRoles.Add(subRole); + + foreach (SubRole subRole in allSubRoles) { + IEnumerable subRoleMembers = await approvalService.GetApprovalGroupMembers(subRole.SubRoleID); + + foreach (User member in subRoleMembers) { + Approval approval = new() { + IssueID = pcrb.PlanNumber, + RoleName = subRole.SubRoleCategoryItem, + SubRole = subRole.SubRoleName, + UserID = member.UserID, + SubRoleID = subRole.SubRoleID, + AssignedDate = DateTime.Now, + Step = followUp.Step, + TaskID = followUp.ID + }; + + await approvalService.CreateApproval(approval); + + approvals = await approvalService.GetApprovalsForIssueId(pcrb.PlanNumber, true); + + approval = approvals.Where(a => a.TaskID == followUp.ID && + a.RoleName.Equals(subRole.SubRoleCategoryItem) && + a.UserID == member.UserID && + a.Step == followUp.Step).First(); + + PCRBNotification notification = new() { + PCRB = pcrb, + Subject = $"[PCRB Follow Up] {pcrb.PlanNumber} - {pcrb.Title}", + Message = $"Follow up for PCRB# {pcrb.PlanNumber} - {pcrb.Title} has been submitted for closure.", + Approval = approval + }; + + await pcrbService.NotifyApprover(notification); + } + } + + string comments = "Submitted for closure"; + + PCRBFollowUpComment comment = new() { + PlanNumber = followUp.PlanNumber, + FollowUpID = followUp.ID, + Comment = comments, + UserID = authStateProvider.CurrentUser.UserID + }; + + await pcrbService.CreateFollowUpComment(comment); + + snackbar.Add("Follow up submitted for closure", Severity.Success); + } else { + throw new Exception("no follow ups available to mark as pending closure"); + } + + followUpSubmitInProcess = false; + } catch (Exception ex) { + followUpSubmitInProcess = false; + snackbar.Add($"Unable to submit follow up for closure, because {ex.Message}", Severity.Error); + } + + StateHasChanged(); + await OnParametersSetAsync(); + } + } + + private async Task ApproveFollowUp() { + if (!followUpApproveInProcess) { + try { + followUpApproveInProcess = true; + + IEnumerable step5Approvals = approvals.Where(a => a.Step == 5 && a.ItemStatus == 0); + foreach (Approval approval in step5Approvals) { + approval.ItemStatus = 1; + approval.Comments = "Follow up complete"; + approval.CompletedDate = DateTime.Now; + await approvalService.UpdateApproval(approval); + } + + foreach (PCRBFollowUp? followUp in pcrb.FollowUps.Where(f => !f.IsComplete)) { + followUp.IsComplete = true; + followUp.IsPendingApproval = false; + followUp.CompletedDate = DateTime.Now; + await pcrbService.UpdateFollowUp(followUp); + } + + pcrb.CurrentStep = (int)PCRB.StagesEnum.Closed; + await pcrbService.UpdatePCRB(pcrb); + + string comments = "Follow up complete"; + + PCRBFollowUpComment comment = new() { + PlanNumber = pcrb.PlanNumber, + FollowUpID = pcrb.FollowUps.First().ID, + Comment = comments, + UserID = authStateProvider.CurrentUser.UserID + }; + + await pcrbService.CreateFollowUpComment(comment); + + PCRBNotification notification = new() { + PCRB = pcrb, + Message = $"Follow up for PCRB# {pcrb.PlanNumber} - {pcrb.Title} has been closed." + }; + + await pcrbService.NotifyOriginator(notification); + + followUpApproveInProcess = false; + + snackbar.Add("Follow up successfully approved", Severity.Success); + } catch (Exception ex) { + followUpApproveInProcess = false; + snackbar.Add($"Unable to approve follow up, because {ex.Message}", Severity.Error); + } + + StateHasChanged(); + await OnParametersSetAsync(); + } + } + + private async Task DenyFollowUp(string action) { + if (!followUpDenyInProcess) { + try { + string pastAction = action.ToLower().Equals("recall") ? "recalled" : "rejected"; + + followUpDenyInProcess = true; + + string comments = ""; + + DialogParameters parameters = new DialogParameters { { x => x.comments, comments } }; + var dialog = await dialogService.ShowAsync($"Follow Up {action} Comment", parameters); + + DialogResult? result = await dialog.Result; + + if (result is null || result.Canceled || result.Data is null || string.IsNullOrWhiteSpace(result.Data?.ToString())) { + throw new Exception("you must provide a comment"); + } + + comments = result.Data?.ToString() ?? string.Empty; + + comments = comments.Trim(); + + IEnumerable step5Approvals = approvals.Where(a => a.Step == 5 && + a.ItemStatus == 0 && + a.UserID != pcrb.OwnerID); + foreach (Approval approval in step5Approvals) { + approval.ItemStatus = -1; + approval.CompletedDate = DateTime.Now; + approval.Comments = comments is null ? string.Empty : comments; + await approvalService.UpdateApproval(approval); + } + + foreach (var followUp in pcrb.FollowUps.Where(f => f.IsPendingApproval)) { + followUp.IsPendingApproval = false; + await pcrbService.UpdateFollowUp(followUp); + } + + StringBuilder messageBuilder = new(); + messageBuilder.Append($"Follow up for PCRB# {pcrb.PlanNumber} - {pcrb.Title} has been {pastAction}. "); + messageBuilder.Append($"Please review the comments and make the necessary revisions. Comments: {comments}"); + + PCRBNotification notification = new() { + PCRB = pcrb, + Message = messageBuilder.ToString() + }; + + await pcrbService.NotifyOriginator(notification); + + comments = $"Follow up {pastAction}. Comments: {comments}"; + + PCRBFollowUpComment comment = new() { + PlanNumber = pcrb.PlanNumber, + FollowUpID = pcrb.FollowUps.First().ID, + Comment = comments, + UserID = authStateProvider.CurrentUser.UserID + }; + + await pcrbService.CreateFollowUpComment(comment); + + followUpDenyInProcess = false; + + snackbar.Add($"Follow up successfully {pastAction}", Severity.Success); + } catch (Exception ex) { + followUpDenyInProcess = false; + snackbar.Add($"Unable to {action.ToLower()} follow up, because {ex.Message}", Severity.Error); + } + + StateHasChanged(); + await OnParametersSetAsync(); + } + } } diff --git a/MesaFabApproval.Client/Services/ApprovalService.cs b/MesaFabApproval.Client/Services/ApprovalService.cs index c1a5744..391dc52 100644 --- a/MesaFabApproval.Client/Services/ApprovalService.cs +++ b/MesaFabApproval.Client/Services/ApprovalService.cs @@ -13,6 +13,7 @@ public interface IApprovalService { Task> GetApprovalGroupMembers(int subRoleId); Task CreateApproval(Approval approval); Task UpdateApproval(Approval approval); + Task DeleteApproval(int approvalID); Task Approve(Approval approval); Task Deny(Approval approval); Task> GetApprovalsForIssueId(int issueId, bool bypassCache); @@ -156,6 +157,20 @@ public class ApprovalService : IApprovalService { await GetApprovalsForUserId(approval.UserID, true); } + public async Task DeleteApproval(int approvalID) { + if (approvalID <= 0) throw new ArgumentException("Invalid approval ID"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Delete, $"approval?approvalID={approvalID}"); + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) { + throw new Exception($"Unable to delete approval, because {responseMessage.ReasonPhrase}"); + } + } + public async Task Approve(Approval approval) { if (approval is null) throw new ArgumentNullException("approval cannot be null"); diff --git a/MesaFabApproval.Client/Services/MesaFabApprovalAuthStateProvider.cs b/MesaFabApproval.Client/Services/MesaFabApprovalAuthStateProvider.cs index 353a83d..ef404b8 100644 --- a/MesaFabApproval.Client/Services/MesaFabApprovalAuthStateProvider.cs +++ b/MesaFabApproval.Client/Services/MesaFabApprovalAuthStateProvider.cs @@ -2,6 +2,7 @@ using MesaFabApproval.Shared.Models; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using MudBlazor; @@ -12,18 +13,22 @@ public class MesaFabApprovalAuthStateProvider : AuthenticationStateProvider, IDi private readonly IAuthenticationService _authService; private readonly IUserService _userService; private readonly ISnackbar _snackbar; + private readonly NavigationManager _navigationManager; public User? CurrentUser { get; private set; } public MesaFabApprovalAuthStateProvider(IAuthenticationService authService, ISnackbar snackbar, - IUserService userService) { + IUserService userService, + NavigationManager navigationManager) { _authService = authService ?? throw new ArgumentNullException("IAuthenticationService not injected"); _snackbar = snackbar ?? throw new ArgumentNullException("ISnackbar not injected"); _userService = userService ?? throw new ArgumentNullException("IUserService not injected"); + _navigationManager = navigationManager ?? + throw new ArgumentNullException("NavigationManager not injected"); AuthenticationStateChanged += OnAuthenticationStateChangedAsync; } diff --git a/MesaFabApproval.Client/Services/PCRBService.cs b/MesaFabApproval.Client/Services/PCRBService.cs index 23a9ac0..dfb2cc9 100644 --- a/MesaFabApproval.Client/Services/PCRBService.cs +++ b/MesaFabApproval.Client/Services/PCRBService.cs @@ -36,6 +36,7 @@ public interface IPCRBService { Task UpdatePCR3Document(PCR3Document document); Task> GetPCR3DocumentsForPlanNumber(int planNumber, bool bypassCache); Task NotifyNewApprovals(PCRB pcrb); + Task NotifyApprover(PCRBNotification notification); Task NotifyApprovers(PCRBNotification notification); Task NotifyOriginator(PCRBNotification notification); Task NotifyResponsiblePerson(PCRBActionItemNotification notification); @@ -43,6 +44,10 @@ public interface IPCRBService { Task> GetFollowUpsByPlanNumber(int planNumber, bool bypassCache); Task UpdateFollowUp(PCRBFollowUp followUp); Task DeleteFollowUp(int id); + Task CreateFollowUpComment(PCRBFollowUpComment comment); + Task> GetFollowUpCommentsByPlanNumber(int planNumber, bool bypassCache); + Task UpdateFollowUpComment(PCRBFollowUpComment comment); + Task DeleteFollowUpComment(int id); } public class PCRBService : IPCRBService { @@ -712,6 +717,26 @@ public class PCRBService : IPCRBService { throw new Exception($"Unable to notify new PCRB approvers, because {responseMessage.ReasonPhrase}"); } + public async Task NotifyApprover(PCRBNotification notification) { + if (notification is null) throw new ArgumentNullException("notification cannot be null"); + if (notification.PCRB is null) throw new ArgumentNullException("PCRB cannot be null"); + if (notification.Approval is null) throw new ArgumentNullException("approval cannot be null"); + if (string.IsNullOrWhiteSpace(notification.Message)) throw new ArgumentException("message cannot be null or empty"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Post, $"pcrb/notify/approver") { + Content = new StringContent(JsonSerializer.Serialize(notification), + Encoding.UTF8, + "application/json") + }; + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) + throw new Exception($"Unable to notify PCRB approver, because {responseMessage.ReasonPhrase}"); + } + public async Task NotifyApprovers(PCRBNotification notification) { if (notification is null) throw new ArgumentNullException("notification cannot be null"); if (notification.PCRB is null) throw new ArgumentNullException("PCRB cannot be null"); @@ -812,7 +837,7 @@ public class PCRBService : IPCRBService { new List(); if (followUps.Count() > 0) - _cache.Set($"pcrbFollowUps{planNumber}", followUps, DateTimeOffset.Now.AddMinutes(5)); + _cache.Set($"pcrbFollowUps{planNumber}", followUps, DateTimeOffset.Now.AddHours(1)); } else { throw new Exception(responseMessage.ReasonPhrase); } @@ -836,6 +861,8 @@ public class PCRBService : IPCRBService { if (!responseMessage.IsSuccessStatusCode) throw new Exception(responseMessage.ReasonPhrase); + + await GetFollowUpsByPlanNumber(followUp.PlanNumber, true); } public async Task DeleteFollowUp(int id) { @@ -849,4 +876,93 @@ public class PCRBService : IPCRBService { if (!responseMessage.IsSuccessStatusCode) throw new Exception(responseMessage.ReasonPhrase); } + + public async Task CreateFollowUpComment(PCRBFollowUpComment comment) { + if (comment is null) throw new ArgumentNullException("comment up cannot be null"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Post, $"pcrb/followUpComment") { + Content = new StringContent(JsonSerializer.Serialize(comment), + Encoding.UTF8, + "application/json") + }; + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) + throw new Exception(responseMessage.ReasonPhrase); + + await GetFollowUpCommentsByPlanNumber(comment.PlanNumber, true); + } + + public async Task> GetFollowUpCommentsByPlanNumber(int planNumber, bool bypassCache) { + if (planNumber <= 0) throw new ArgumentException($"{planNumber} is not a valid PCRB Plan#"); + + IEnumerable? comments = null; + if (!bypassCache) + comments = _cache.Get>($"pcrbFollowUpComments{planNumber}"); + + if (comments is null) { + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = + new(HttpMethod.Get, $"pcrb/followUpComments?planNumber={planNumber}&bypassCache={bypassCache}"); + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (responseMessage.IsSuccessStatusCode) { + string responseContent = await responseMessage.Content.ReadAsStringAsync(); + + JsonSerializerOptions jsonSerializerOptions = new() { + PropertyNameCaseInsensitive = true + }; + + comments = JsonSerializer.Deserialize>(responseContent, jsonSerializerOptions) ?? + new List(); + + if (comments.Count() > 0) { + foreach (PCRBFollowUpComment comment in comments) + comment.User = await _userService.GetUserByUserId(comment.UserID); + + _cache.Set($"pcrbFollowUpComments{planNumber}", comments, DateTimeOffset.Now.AddHours(1)); + } + } else { + throw new Exception(responseMessage.ReasonPhrase); + } + } + + return comments; + } + + public async Task UpdateFollowUpComment(PCRBFollowUpComment comment) { + if (comment is null) throw new ArgumentNullException("comment up cannot be null"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Put, $"pcrb/followUpComment") { + Content = new StringContent(JsonSerializer.Serialize(comment), + Encoding.UTF8, + "application/json") + }; + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) + throw new Exception(responseMessage.ReasonPhrase); + + await GetFollowUpCommentsByPlanNumber(comment.PlanNumber, true); + } + + public async Task DeleteFollowUpComment(int id) { + if (id <= 0) throw new ArgumentException($"{id} is not a valid PCRB follow up comment ID"); + + HttpClient httpClient = _httpClientFactory.CreateClient("API"); + + HttpRequestMessage requestMessage = new(HttpMethod.Delete, $"pcrb/followUpComment?id={id}"); + + HttpResponseMessage responseMessage = await httpClient.SendAsync(requestMessage); + + if (!responseMessage.IsSuccessStatusCode) throw new Exception(responseMessage.ReasonPhrase); + } } diff --git a/MesaFabApproval.Client/Utilities/ApiHttpClientHandler.cs b/MesaFabApproval.Client/Utilities/ApiHttpClientHandler.cs index d81c3fc..f071370 100644 --- a/MesaFabApproval.Client/Utilities/ApiHttpClientHandler.cs +++ b/MesaFabApproval.Client/Utilities/ApiHttpClientHandler.cs @@ -111,7 +111,7 @@ public class ApiHttpClientHandler : DelegatingHandler { string? redirectUrl = _cache.Get("redirectUrl"); if (!string.IsNullOrWhiteSpace(redirectUrl)) { - _navigationManager.NavigateTo($"login/{redirectUrl}"); + _navigationManager.NavigateTo($"login?redirectPath={redirectUrl}"); } else { _navigationManager.NavigateTo("login"); } diff --git a/MesaFabApproval.Shared/Models/PCRB.cs b/MesaFabApproval.Shared/Models/PCRB.cs index 87e5de8..5dd7e34 100644 --- a/MesaFabApproval.Shared/Models/PCRB.cs +++ b/MesaFabApproval.Shared/Models/PCRB.cs @@ -8,9 +8,21 @@ public class PCRB { "PCR1", "PCR2", "PCR3", - "Complete" + "Complete", + "Follow Up", + "Closed" ]; + public enum StagesEnum { + Draft = 0, + PCR1 = 1, + PCR2 = 2, + PCR3 = 3, + Complete = 4, + FollowUp = 5, + Closed = 6 + } + public int PlanNumber { get; set; } public int OwnerID { get; set; } public string OwnerName { get; set; } = ""; @@ -24,4 +36,5 @@ public class PCRB { public DateTime LastUpdateDate { get; set; } = DateTimeUtilities.MIN_DT; public DateTime ClosedDate { get; set; } = DateTimeUtilities.MAX_DT; public string Type { get; set; } = ""; + public IEnumerable FollowUps { get; set; } = new List(); } \ No newline at end of file diff --git a/MesaFabApproval.Shared/Models/PCRBFollowUp.cs b/MesaFabApproval.Shared/Models/PCRBFollowUp.cs index e90db8c..b01e0ad 100644 --- a/MesaFabApproval.Shared/Models/PCRBFollowUp.cs +++ b/MesaFabApproval.Shared/Models/PCRBFollowUp.cs @@ -9,6 +9,7 @@ public class PCRBFollowUp { public required DateTime FollowUpDate { get; set; } public bool IsComplete { get; set; } = false; public bool IsDeleted { get; set; } = false; + public bool IsPendingApproval { get; set; } = false; public DateTime CompletedDate { get; set; } = DateTimeUtilities.MAX_DT; - public string Comments { get; set; } = string.Empty; + public DateTime UpdateDate { get; set; } = DateTime.Now; } diff --git a/MesaFabApproval.Shared/Models/PCRBFollowUpComment.cs b/MesaFabApproval.Shared/Models/PCRBFollowUpComment.cs new file mode 100644 index 0000000..2da6ccd --- /dev/null +++ b/MesaFabApproval.Shared/Models/PCRBFollowUpComment.cs @@ -0,0 +1,13 @@ +using MesaFabApproval.Shared.Utilities; + +namespace MesaFabApproval.Shared.Models; + +public class PCRBFollowUpComment { + public int ID { get; set; } + public required int PlanNumber { get; set; } + public required int FollowUpID { get; set; } + public DateTime CommentDate { get; set; } = DateTime.Now; + public required int UserID { get; set; } + public User? User { get; set; } + public string Comment { get; set; } = string.Empty; +} diff --git a/MesaFabApproval.Shared/Models/PCRBNotification.cs b/MesaFabApproval.Shared/Models/PCRBNotification.cs index 72795cc..b56a195 100644 --- a/MesaFabApproval.Shared/Models/PCRBNotification.cs +++ b/MesaFabApproval.Shared/Models/PCRBNotification.cs @@ -2,5 +2,8 @@ public class PCRBNotification { public required string Message { get; set; } + public string? Subject { get; set; } public required PCRB PCRB { get; set; } + public Approval? Approval { get; set; } + public bool NotifyQaPreApprover { get; set; } = false; } \ No newline at end of file