From f0633253094cb73a6801cfa1ac008dd986d52214 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 11 Dec 2024 13:31:50 +0100 Subject: [PATCH 1/9] fix: Incorrect url --- .../Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs index b0103e1ac..16541c9cc 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs @@ -315,8 +315,8 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje } }, new GoToAction() { - Title = "Go to positions listing view", - Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view?filter=tbn-pos-3m" + Title = "Go to positions timeline view", + Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/timeline-view?filter=tbn-pos-3m" }) .Build(); From b6fdea260bdeff38d60c7d189dfbc82015b24675 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 12 Dec 2024 09:36:13 +0100 Subject: [PATCH 2/9] feat: Updated calculation logic for report --- .../WeeklyTaskOwnerReportDataCreator.cs | 32 ++++++++----------- .../WeeklyTaskOwnerReportDataCreator.cs | 13 ++++---- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs index 056daad10..e03783926 100644 --- a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs @@ -12,10 +12,12 @@ public abstract class WeeklyTaskOwnerReportDataCreator // Logic taken/inspired from the frontend // https://github.com/equinor/fusion-resource-allocation-apps/blob/a9330b2aa8d104e51536692a72334252d5e474e1/apps/org-admin/src/pages/ProjectPage/components/ChartComponent/components/utils.ts#L28 + // https://github.com/equinor/fusion-resource-allocation-apps/blob/0c8477f48021c594af20c0b1ba7b549b187e2e71/apps/org-admin/src/pages/ProjectPage/pages/EditPositionsPage/pages/TimelineViewPage/components/TimelineFilter/selectors/positionSelector.ts#L14 public static List GetTBNPositionsStartingWithinThreeMonths(IEnumerable allProjectPositions, ICollection requests) { var nowDate = NowDate; + var expiringDate = nowDate.AddMonths(3); var tbnPositions = new List(); @@ -24,28 +26,20 @@ public static List GetTBNPositionsStartingWithinThreeMonths(IEnumer if (IsSupportPosition(position)) continue; - var isPositionActive = position.Instances.Any(i => i.AppliesFrom.Date <= nowDate.Date && i.AppliesTo.Date >= nowDate.Date); - - if (isPositionActive) - continue; - - var futureInstances = position.Instances.Where(i => i.AppliesFrom.Date >= nowDate.Date).ToList(); - - if (futureInstances.Count == 0) - continue; - - var startingInstance = futureInstances.MinBy(i => i.AppliesFrom); // Get the instance starting soonest + var expiringInstance = position.Instances + .OrderBy(i => i.AppliesFrom) + .Where(i => i.AppliesFrom < expiringDate) // hasDueWithinThreeMonths + .FirstOrDefault(i => i.AppliesTo >= nowDate); // !isInstancePast - if (startingInstance is null) + if (expiringInstance is null) continue; - var instanceHasPersonalRequest = requests.Any(r => r.OrgPositionInstance?.Id == startingInstance.Id); + var hasPersonAssigned = expiringInstance.AssignedPerson is not null; - if (instanceHasPersonalRequest) + if (hasPersonAssigned) continue; - if (startingInstance.AppliesFrom.Date < nowDate.AddMonths(3).Date && startingInstance.AssignedPerson is null) - tbnPositions.Add(new TBNPosition(position, startingInstance.AppliesFrom)); + tbnPositions.Add(new TBNPosition(position, expiringInstance.AppliesFrom)); } return tbnPositions; @@ -63,7 +57,7 @@ public static List GetPositionAllocationsEndingNextThreeMonths /* * Remember that it's the position*Allocation* that is expiring, not the position itself. * So a position allocation can be considered expiring if: - * 1. The position is active and the next split within the next 3 months does not have a person assigned + * 1. The position is active with an assigned person, and the next split within the next 3 months does not have a person assigned * - We want to notify task owners that an upcoming split is missing an allocation and that they should assign someone * * 2. The last split is expiring within the next 3 months. @@ -85,7 +79,7 @@ public static List GetPositionAllocationsEndingNextThreeMonths if (position.Instances.Count == 0) // No instances, skip continue; - var activeInstance = position.Instances.FirstOrDefault(i => i.AppliesFrom <= nowDate && i.AppliesTo >= nowDate); + var activeInstance = position.Instances.FirstOrDefault(i => i.AppliesFrom <= nowDate && i.AppliesTo >= nowDate && i.AssignedPerson is not null); // Find future instances with a start date within the 3-month window that may or may not end within the 3-month window @@ -110,7 +104,7 @@ public static List GetPositionAllocationsEndingNextThreeMonths // If the first TBN/expiring instance found is not the last instance, then there are more instances after it var isEndingInstanceLast = futureInstances.Last() == endingPositionAllocation; - if (isEndingInstanceLast || endingPositionAllocation.AssignedPerson is null) + if ((isEndingInstanceLast || endingPositionAllocation.AssignedPerson is null) && endingPositionAllocation.AppliesTo < expiringDate) expiringPositions.Add(new ExpiringPosition(position, endingPositionAllocation.AppliesTo)); continue; diff --git a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs index e5d3571b4..4a26b36db 100644 --- a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs @@ -153,18 +153,19 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() AddPosition(manySmallInstancesWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); - var endingPosition = new PositionBuilder() - .WithInstance(Past, now.AddMonths(2)) - .Build(); - AddPosition(endingPosition, shouldBeIncludedInReportList: true); - - var nonEndingPosition = new PositionBuilder() .WithInstance(Past, now.AddMonths(2), person: personA) .AddNextInstance(TimeSpan.FromDays(31), person: personB) .Build(); AddPosition(nonEndingPosition); + var nonActivePositionWithPastAllocationAndFutureTBN = new PositionBuilder() + .WithInstance(now.AddDays(-3), now.AddDays(-2), person: personA) + .AddNextInstance(now.AddDays(80), now.AddDays(120)) + .Build(); + + AddPosition(nonActivePositionWithPastAllocationAndFutureTBN); + if (shouldBeIncludedInReport.Distinct().Count() != shouldBeIncludedInReport.Count) throw new InvalidOperationException($"Test setup error: Duplicate position names in {nameof(shouldBeIncludedInReport)}"); From 4ebbe0cc45533abb8d0766195cda37c1525d5d5a Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:37:44 +0100 Subject: [PATCH 3/9] Refactoring + updated TBNPositionsStartingWithinThreeMonths calculation --- .../WeeklyTaskOwnerReportWorker.cs | 2 +- .../WeeklyTaskOwnerReportDataCreator.cs | 11 +- .../WeeklyTaskOwnerReportDataCreator.cs | 176 ++++++++++-------- 3 files changed, 107 insertions(+), 82 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs index 9186367ca..fd30c98c5 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs @@ -69,7 +69,7 @@ private async Task CreateAndStoreReportAsync(WeeklyTaskOwnerReportMessage messag var expiringAdmins = WeeklyTaskOwnerReportDataCreator.GetExpiringAdmins(admins); var actionsAwaitingTaskOwner = WeeklyTaskOwnerReportDataCreator.GetActionsAwaitingTaskOwnerAsync(activeRequestsForProject); var expiringPositionAllocations = WeeklyTaskOwnerReportDataCreator.GetPositionAllocationsEndingNextThreeMonths(allProjectPositions); - var tbnPositions = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(allProjectPositions, activeRequestsForProject); + var tbnPositions = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(allProjectPositions); var lastMonday = now.GetPreviousWeeksMondayDate(); var report = new ApiWeeklyTaskOwnerReport() diff --git a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs index e03783926..c913614df 100644 --- a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs @@ -13,8 +13,7 @@ public abstract class WeeklyTaskOwnerReportDataCreator // Logic taken/inspired from the frontend // https://github.com/equinor/fusion-resource-allocation-apps/blob/a9330b2aa8d104e51536692a72334252d5e474e1/apps/org-admin/src/pages/ProjectPage/components/ChartComponent/components/utils.ts#L28 // https://github.com/equinor/fusion-resource-allocation-apps/blob/0c8477f48021c594af20c0b1ba7b549b187e2e71/apps/org-admin/src/pages/ProjectPage/pages/EditPositionsPage/pages/TimelineViewPage/components/TimelineFilter/selectors/positionSelector.ts#L14 - public static List GetTBNPositionsStartingWithinThreeMonths(IEnumerable allProjectPositions, - ICollection requests) + public static List GetTBNPositionsStartingWithinThreeMonths(IEnumerable allProjectPositions) { var nowDate = NowDate; var expiringDate = nowDate.AddMonths(3); @@ -29,16 +28,12 @@ public static List GetTBNPositionsStartingWithinThreeMonths(IEnumer var expiringInstance = position.Instances .OrderBy(i => i.AppliesFrom) .Where(i => i.AppliesFrom < expiringDate) // hasDueWithinThreeMonths - .FirstOrDefault(i => i.AppliesTo >= nowDate); // !isInstancePast + .Where(i => i.AssignedPerson is null) // TBN instance + .FirstOrDefault(i => nowDate <= i.AppliesTo); if (expiringInstance is null) continue; - var hasPersonAssigned = expiringInstance.AssignedPerson is not null; - - if (hasPersonAssigned) - continue; - tbnPositions.Add(new TBNPosition(position, expiringInstance.AppliesFrom)); } diff --git a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs index 4a26b36db..f1574a6a7 100644 --- a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs @@ -45,6 +45,9 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() { #region Arrange + var testData = new ReportTestDataContainer(); + + var personA = new ApiPersonV2() { AzureUniqueId = Guid.NewGuid(), @@ -56,75 +59,71 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() Name = "Test NameB" }; - var shouldBeIncludedInReport = new List(); - var positionsToTest = new List(); - var instanceToBeIncluded = new Dictionary(); - var activeWithFutureInstance = new PositionBuilder() .WithInstance(Past, now.AddDays(30 * 1.5), person: personA) .AddNextInstance(TimeSpan.FromDays(30 * 4), person: personA) .Build(); - AddPosition(activeWithFutureInstance); + testData.AddPosition(activeWithFutureInstance); var activeWithoutFutureInstance = new PositionBuilder() .WithInstance(Past, now.AddDays(30), person: personA) .AddNextInstance(TimeSpan.FromDays(30), person: personA, extId: "1") .Build(); - AddPosition(activeWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(activeWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var activeWithFutureInstanceDifferentPerson = new PositionBuilder() .WithInstance(Past, now.AddDays(30 * 1.5), person: personA) .AddNextInstance(TimeSpan.FromDays(30 * 4), person: personB) .Build(); - AddPosition(activeWithFutureInstanceDifferentPerson); + testData.AddPosition(activeWithFutureInstanceDifferentPerson); var singleActiveWithoutFutureInstance = new PositionBuilder() .WithInstance(Past, now.Add(TimeSpan.FromDays(30 * 1.5)), person: personA) .Build(); - AddPosition(singleActiveWithoutFutureInstance, shouldBeIncludedInReportList: true); + testData.AddPosition(singleActiveWithoutFutureInstance, shouldBeIncludedInReportList: true); var activeWithFutureInstanceUnassignedPerson = new PositionBuilder() .WithInstance(Past, now.AddDays(30 * 2), person: personA) .AddNextInstance(TimeSpan.FromDays(30 * 2), person: null) .Build(); - AddPosition(activeWithFutureInstanceUnassignedPerson, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); + testData.AddPosition(activeWithFutureInstanceUnassignedPerson, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); var futureInstanceThatIsAlsoExpiring = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) .Build(); - AddPosition(futureInstanceThatIsAlsoExpiring, shouldBeIncludedInReportList: true); + testData.AddPosition(futureInstanceThatIsAlsoExpiring, shouldBeIncludedInReportList: true); var futureInstancesThatIsAlsoExpiring = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) .AddNextInstance(TimeSpan.FromDays(1), person: personA, extId: "1") .Build(); - AddPosition(futureInstancesThatIsAlsoExpiring, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(futureInstancesThatIsAlsoExpiring, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var futureInstanceThatIsMissingAllocation = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) .AddNextInstance(TimeSpan.FromDays(1), person: null) .Build(); - AddPosition(futureInstanceThatIsMissingAllocation, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); + testData.AddPosition(futureInstanceThatIsMissingAllocation, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); var futureInstancesWhereOneIsTBN = new PositionBuilder() .WithInstance(now.AddDays(10), now.AddDays(30), person: personA) .AddNextInstance(TimeSpan.FromDays(2), person: null) .AddNextInstance(TimeSpan.FromDays(6), person: personA) .Build(); - AddPosition(futureInstancesWhereOneIsTBN, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); + testData.AddPosition(futureInstancesWhereOneIsTBN, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); var futureInstanceThatIsNotExpiring = new PositionBuilder() .WithInstance(now.AddDays(30), now.AddDays(60), person: personA) .AddNextInstance(TimeSpan.FromDays(100), person: personA) .Build(); - AddPosition(futureInstanceThatIsNotExpiring); + testData.AddPosition(futureInstanceThatIsNotExpiring); // Entire gap/time-period is within the 3-month window @@ -133,7 +132,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() .AddNextInstance(now.AddDays(30 * 2), now.AddDays(30 * 4), person: personA) .Build(); - AddPosition(activePositionWithFutureInstanceWithSmallGap); + testData.AddPosition(activePositionWithFutureInstanceWithSmallGap); var activePositionWithFutureInstanceWithLargerGap = new PositionBuilder() @@ -141,7 +140,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() .AddNextInstance(now.AddDays(30 * 5), now.AddDays(30 * 7), person: personA) .AddNextInstance(TimeSpan.FromDays(10), person: personA) .Build(); - AddPosition(activePositionWithFutureInstanceWithLargerGap, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(activePositionWithFutureInstanceWithLargerGap, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var manySmallInstancesWithoutFutureInstance = new PositionBuilder() @@ -150,21 +149,25 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() .AddNextInstance(TimeSpan.FromDays(10), person: personA) .AddNextInstance(TimeSpan.FromDays(10), person: personB, extId: "1") .Build(); - AddPosition(manySmallInstancesWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); + testData.AddPosition(manySmallInstancesWithoutFutureInstance, shouldBeIncludedInReportList: true, instanceSelector: i => i.ExternalId == "1"); var nonEndingPosition = new PositionBuilder() .WithInstance(Past, now.AddMonths(2), person: personA) .AddNextInstance(TimeSpan.FromDays(31), person: personB) .Build(); - AddPosition(nonEndingPosition); + testData.AddPosition(nonEndingPosition); var nonActivePositionWithPastAllocationAndFutureTBN = new PositionBuilder() .WithInstance(now.AddDays(-3), now.AddDays(-2), person: personA) .AddNextInstance(now.AddDays(80), now.AddDays(120)) .Build(); - AddPosition(nonActivePositionWithPastAllocationAndFutureTBN); + testData.AddPosition(nonActivePositionWithPastAllocationAndFutureTBN); + + var shouldBeIncludedInReport = testData.ShouldBeIncludedInReport; + var positionsToTest = testData.PositionsToTest; + var instanceToBeIncluded = testData.InstanceToBeIncluded; if (shouldBeIncludedInReport.Distinct().Count() != shouldBeIncludedInReport.Count) throw new InvalidOperationException($"Test setup error: Duplicate position names in {nameof(shouldBeIncludedInReport)}"); @@ -189,99 +192,125 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() } // Check that there are no extra positions that should not be included - data.Should().HaveSameCount(shouldBeIncludedInReport, "All positions that should be included in the report should be included"); - return; - - // Helper method - void AddPosition(ApiPositionV2 position, bool shouldBeIncludedInReportList = false, Func? instanceSelector = null, [CallerArgumentExpression("position")] string positionName = null!) - { - ArgumentNullException.ThrowIfNull(position); - - if (shouldBeIncludedInReportList) - shouldBeIncludedInReport.Add(positionName); - - positionsToTest.Add(position); - position.Name = positionName; - - if (shouldBeIncludedInReportList && instanceSelector is not null) - { - var instances = position.Instances.Where(instanceSelector).ToArray(); - - if (instances.Length == 0) - throw new InvalidOperationException($"Test setup error: No instance found for position {positionName} that matches the selector"); - - if (instances.Length > 1) - throw new InvalidOperationException($"Test setup error: Multiple instances found for position {positionName} that matches the selector"); - - instanceToBeIncluded.Add(position, instances.First()); - } - } + data.Should().HaveSameCount(shouldBeIncludedInReport, $"Exactly these positions should be included in the report, {string.Join(", ", shouldBeIncludedInReport)}, these should not be included {string.Join(", ", positionsToTest.Select(p => p.Name).Except(shouldBeIncludedInReport))}"); } [Fact] public void GetTBNPositionsStartingWithinThreeMonthsTests() { + var testData = new ReportTestDataContainer(); + var person = new ApiPersonV2() { AzureUniqueId = Guid.NewGuid(), Name = "Test Name" }; - var activePositions = + var activePositionWithPerson = new PositionBuilder() - .WithInstance(now.Subtract(TimeSpan.FromDays(1)), now.AddMonths(2)) - .AddNextInstance(TimeSpan.FromDays(26)) + .WithInstance(Past, now.AddMonths(2), person: person) + .AddNextInstance(TimeSpan.FromDays(40), person: person) .Build(); - activePositions.Name = nameof(activePositions); + testData.AddPosition(activePositionWithPerson); var nonActiveWithinThreeMonthsWithPerson = new PositionBuilder() .WithInstance(now.AddMonths(2), now.AddMonths(3), person) .AddNextInstance(TimeSpan.FromDays(26)) + .AddNextInstance(TimeSpan.FromDays(26)) .Build(); - nonActiveWithinThreeMonthsWithPerson.Name = nameof(nonActiveWithinThreeMonthsWithPerson); + testData.AddPosition(nonActiveWithinThreeMonthsWithPerson); + + var activePositionWithPersonButFutureWithoutPerson = + new PositionBuilder() + .WithInstance(Past, now.AddMonths(2), person: person) + .AddNextInstance(TimeSpan.FromDays(40), extId: "1") + .AddNextInstance(TimeSpan.FromDays(40)) + .Build(); + testData.AddPosition(activePositionWithPersonButFutureWithoutPerson, shouldBeIncludedInReportList: true, i => i.ExternalId == "1"); - var nonActiveWithinThreeMonthsNoPersonButHasRequest = + var nonActiveWithinThreeMonths = new PositionBuilder() .WithInstance(now.AddMonths(2), now.AddMonths(3)) + .AddNextInstance(TimeSpan.FromDays(26), person) .Build(); - nonActiveWithinThreeMonthsNoPersonButHasRequest.Name = nameof(nonActiveWithinThreeMonthsNoPersonButHasRequest); + testData.AddPosition(nonActiveWithinThreeMonths, shouldBeIncludedInReportList: true, instanceSelector: i => i.AssignedPerson is null); - var request = new IResourcesApiClient.ResourceAllocationRequest() - { - Id = Guid.NewGuid(), - OrgPosition = new() - { - Id = nonActiveWithinThreeMonthsNoPersonButHasRequest.Id - } - }; - var nonActiveOutsideThreeMonths = new PositionBuilder() .WithInstance(now.AddMonths(4), now.AddMonths(5)) .Build(); - nonActiveOutsideThreeMonths.Name = nameof(nonActiveOutsideThreeMonths); + testData.AddPosition(nonActiveOutsideThreeMonths); var nonActiveWithinThreeMonthsNoPerson = new PositionBuilder() - .WithInstance(now.AddMonths(2), now.AddMonths(3)) + .WithInstance(now.AddMonths(-3), now.AddMonths(-2)) + .AddNextInstance(now.AddMonths(2), now.AddMonths(3), extId: "1") .Build(); - nonActiveWithinThreeMonthsNoPerson.Name = nameof(nonActiveWithinThreeMonthsNoPerson); + testData.AddPosition(nonActiveWithinThreeMonthsNoPerson, shouldBeIncludedInReportList: true, i => i.ExternalId == "1"); + var pastPositionWithPerson = + new PositionBuilder() + .WithInstance(now.AddMonths(-3), now.AddMonths(-2), person) + .AddNextInstance(now.AddMonths(-2), now.AddMonths(-1)) + .Build(); + testData.AddPosition(pastPositionWithPerson); + + + var data = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(testData.PositionsToTest); + + data.Should().OnlyHaveUniqueItems(); + foreach (var positionName in testData.ShouldBeIncludedInReport) + { + data.Should().ContainSingle(p => p.Position.Name == positionName, $"Position {positionName} should be included in the report"); + } - var data = WeeklyTaskOwnerReportDataCreator.GetTBNPositionsStartingWithinThreeMonths(new List + // Ensure that the starts at date is set correctly + foreach (var (position, apiPositionInstanceV2) in testData.InstanceToBeIncluded) { - activePositions, - nonActiveWithinThreeMonthsWithPerson, - nonActiveWithinThreeMonthsNoPerson, - nonActiveOutsideThreeMonths - }, [request]); + data.Should().ContainSingle(p => p.StartsAt == apiPositionInstanceV2.AppliesFrom && p.Position.Id == position.Id, $"Position {position.Name} should have an instance that starts at {apiPositionInstanceV2.AppliesFrom}"); + } + + // Check that there are no extra positions that should not be included + data.Should().HaveSameCount(testData.ShouldBeIncludedInReport, + $"Exactly these positions should be included in the report, {string.Join(", ", testData.ShouldBeIncludedInReport)}," + + $" these should not be included {string.Join(", ", testData.PositionsToTest.Select(p => p.Name).Except(testData.ShouldBeIncludedInReport))}"); + } + - data.Should().ContainSingle(p => p.Position.Id == nonActiveWithinThreeMonthsNoPerson.Id); + private class ReportTestDataContainer + { + public List ShouldBeIncludedInReport { get; } = new(); + public List PositionsToTest { get; } = new(); + public Dictionary InstanceToBeIncluded { get; } = new(); + + public void AddPosition(ApiPositionV2 position, bool shouldBeIncludedInReportList = false, Func? instanceSelector = null, [CallerArgumentExpression("position")] string positionName = null!) + { + ArgumentNullException.ThrowIfNull(position); + + if (shouldBeIncludedInReportList) + ShouldBeIncludedInReport.Add(positionName); + + PositionsToTest.Add(position); + position.Name = positionName; + + if (shouldBeIncludedInReportList && instanceSelector is not null) + { + var instances = position.Instances.Where(instanceSelector).ToArray(); + + if (instances.Length == 0) + throw new InvalidOperationException($"Test setup error: No instance found for position {positionName} that matches the selector"); + + if (instances.Length > 1) + throw new InvalidOperationException($"Test setup error: Multiple instances found for position {positionName} that matches the selector"); + + InstanceToBeIncluded.Add(position, instances.First()); + } + } } @@ -360,7 +389,8 @@ public InstanceChainBuilder AddNextInstance(DateTime appliesFrom, DateTime appli AssignedPerson = person, Type = type, AppliesFrom = appliesFrom, - AppliesTo = appliesTo + AppliesTo = appliesTo, + ExternalId = extId }); return this; } From 0db7ba0e13cfa574f812056bf982afa5b70efeab Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 13 Dec 2024 14:50:50 +0100 Subject: [PATCH 4/9] feat: Redid calculation --- .../WeeklyTaskOwnerReportDataCreator.cs | 138 ++++++++++-------- .../WeeklyTaskOwnerReportDataCreator.cs | 16 +- 2 files changed, 91 insertions(+), 63 deletions(-) diff --git a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs index c913614df..8d2b276cf 100644 --- a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs @@ -49,21 +49,6 @@ private static bool IsSupportPosition(ApiPositionV2 position) public static List GetPositionAllocationsEndingNextThreeMonths(IEnumerable allProjectPositions) { - /* - * Remember that it's the position*Allocation* that is expiring, not the position itself. - * So a position allocation can be considered expiring if: - * 1. The position is active with an assigned person, and the next split within the next 3 months does not have a person assigned - * - We want to notify task owners that an upcoming split is missing an allocation and that they should assign someone - * - * 2. The last split is expiring within the next 3 months. - * There can be more splits after this but if they're not starting (appliesFrom) within the next 3 months (from NowDate), we consider the position expiring. - * Once the later split comes within the 3-month window, the position will fall under TBNPositionsStartingWithinThreeMonths - * - We want to notify task owners that the position is expiring - * - * Note: If there is a gap between two splits and the gap/time-period is fully within the 3-month window, - * then we DO NOT consider the position expiring. This is also an unusual case. - */ - var nowDate = NowDate; var expiringDate = nowDate.AddMonths(3); @@ -71,79 +56,112 @@ public static List GetPositionAllocationsEndingNextThreeMonths foreach (var position in allProjectPositions) { + position.Instances = position.Instances.OrderBy(i => i.AppliesFrom).ToList(); if (position.Instances.Count == 0) // No instances, skip continue; - var activeInstance = position.Instances.FirstOrDefault(i => i.AppliesFrom <= nowDate && i.AppliesTo >= nowDate && i.AssignedPerson is not null); + var instancesWithinPeriod = FindInstancesWithinPeriod(position, new DateTimeRange() { Start = nowDate, Stop = expiringDate }); - // Find future instances with a start date within the 3-month window that may or may not end within the 3-month window - var futureInstances = position.Instances - .Where(i => i.AppliesFrom >= nowDate && i.AppliesFrom < expiringDate) - .OrderBy(i => i.AppliesFrom) - .ToList(); + if (instancesWithinPeriod.EndingWithinPeriod.Count == 0 + && instancesWithinPeriod.ContainedWithinPeriod.Count == 0 + && instancesWithinPeriod.StartingWithinPeriod.Any()) + { + continue; + } - // Handle case where the position is not currently active - if (activeInstance is null) + foreach (var instance in instancesWithinPeriod.Instances) { - if (futureInstances.Count == 0) - continue; // This is a past position + if ((instancesWithinPeriod.ContainedWithinPeriod.Contains(instance) || instancesWithinPeriod.StartingWithinPeriod.Contains(instance)) + && instance.AssignedPerson is null) + { + expiringPositions.Add(new ExpiringPosition(position, instance.AppliesTo)); + break; + } + + if (instancesWithinPeriod.AnyInstancesAfter(instance) == false) + { + expiringPositions.Add(new ExpiringPosition(position, instance.AppliesTo)); + break; + } + } + } - var endingPositionAllocation = FindFirstTBNOrLastExpiringInstance(futureInstances); - if (endingPositionAllocation is null) - // No TBN/expiring instances found within the 3-month window, could be an instance that starts within the 3-month window - // But ends after the 3-month window - continue; + return expiringPositions; + } - // If the first TBN/expiring instance found is not the last instance, then there are more instances after it - var isEndingInstanceLast = futureInstances.Last() == endingPositionAllocation; + private static InstancesWithinPeriod FindInstancesWithinPeriod(ApiPositionV2 position, DateTimeRange period) + { + var instancesEndingWithinPeriod = position.Instances + .Where(i => period.Contains(i.AppliesTo) && !period.Contains(i.AppliesFrom)) + .ToList(); - if ((isEndingInstanceLast || endingPositionAllocation.AssignedPerson is null) && endingPositionAllocation.AppliesTo < expiringDate) - expiringPositions.Add(new ExpiringPosition(position, endingPositionAllocation.AppliesTo)); + var instancesContainingPeriod = position.Instances + .Where(i => period.Contains(i.AppliesFrom) && period.Contains(i.AppliesTo)) + .ToList(); - continue; - } + var instancesStartingWithinPeriod = position.Instances + .Except(instancesContainingPeriod) + .Where(i => period.Contains(i.AppliesFrom) && !period.Contains(i.AppliesTo)) + .ToList(); + var instancesStartingAfterPeriod = position.Instances + .Where(i => i.AppliesFrom > period.Stop) + .ToList(); - // Handle case where the position is currently active + var lastInstance = instancesStartingAfterPeriod.LastOrDefault(); - var isActiveInstanceExpiring = activeInstance.AppliesTo < expiringDate; - if (isActiveInstanceExpiring && futureInstances.Count == 0) - { - expiringPositions.Add(new ExpiringPosition(position, activeInstance.AppliesTo)); - continue; - } + return new InstancesWithinPeriod() + { + EndingWithinPeriod = instancesEndingWithinPeriod, + ContainedWithinPeriod = instancesContainingPeriod, + StartingWithinPeriod = instancesStartingWithinPeriod, + LastInstance = lastInstance + }; + } - if (isActiveInstanceExpiring) - { - var endingPositionAllocation = FindFirstTBNOrLastExpiringInstance(futureInstances); - if (endingPositionAllocation is not null) - expiringPositions.Add(new ExpiringPosition(position, endingPositionAllocation.AppliesTo)); - } + private class InstancesWithinPeriod + { + public ApiPositionInstanceV2? LastInstance { get; set; } + public required List EndingWithinPeriod { get; set; } + public required List ContainedWithinPeriod { get; set; } + public required List StartingWithinPeriod { get; set; } // Starting within period and ending after period - // The instance is active and not expiring, continue to next position - } + public List Instances => EndingWithinPeriod.Concat(ContainedWithinPeriod).Concat(StartingWithinPeriod).Distinct().ToList(); + public bool AnyInstancesAfter(ApiPositionInstanceV2 instance) + { + // In this case the instance outlasts the period and is outside of scope + if (StartingWithinPeriod.Contains(instance)) + return true; - return expiringPositions; + return Instances.Except([instance]).Any(i => i.AppliesFrom > instance.AppliesTo); + } } - private static ApiPositionInstanceV2? FindFirstTBNOrLastExpiringInstance(IEnumerable futureOrderedInstances) + + private class DateTimeRange { - ApiPositionInstanceV2? lastExpiringInstance = null; - foreach (var instance in futureOrderedInstances) + public DateTime Start { get; set; } + public DateTime Stop { get; set; } + + public bool Overlaps(DateTimeRange other) { - if (instance.AssignedPerson is null) - return instance; // We found a TBN instance + return Start < other.Stop && Stop > other.Start; + } - if (instance.AppliesTo < NowDate.AddMonths(3)) - lastExpiringInstance = instance; // We found an expiring instance + public bool Contains(DateTime dateTime) + { + return Start <= dateTime && dateTime <= Stop; } - return lastExpiringInstance; + public TimeSpan Duration() + { + return Stop - Start; + } } // https://github.com/equinor/fusion-resource-allocation-apps/blob/0c8477f48021c594af20c0b1ba7b549b187e2e71/apps/org-admin/src/pages/ProjectPage/utils.ts#L53 diff --git a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs index f1574a6a7..49e4e862e 100644 --- a/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/tests/Fusion.Summary.Functions.Tests/Notifications/WeeklyTaskOwnerReportDataCreator.cs @@ -93,7 +93,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() var futureInstanceThatIsAlsoExpiring = new PositionBuilder() - .WithInstance(now.AddDays(30), now.AddDays(30 * 2), person: personA) + .WithInstance(now.AddDays(30), now.AddDays(30 * 1.5), person: personA) .Build(); testData.AddPosition(futureInstanceThatIsAlsoExpiring, shouldBeIncludedInReportList: true); @@ -165,6 +165,16 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() testData.AddPosition(nonActivePositionWithPastAllocationAndFutureTBN); + + var testPos = new PositionBuilder() + .WithInstance(now.AddMonths(-6), now.AddMonths(1), person: personA) + .AddNextInstance(TimeSpan.FromDays(48), personA) + .AddNextInstance(TimeSpan.FromDays(90), personA) + .Build(); + + testData.AddPosition(testPos); + + var shouldBeIncludedInReport = testData.ShouldBeIncludedInReport; var positionsToTest = testData.PositionsToTest; var instanceToBeIncluded = testData.InstanceToBeIncluded; @@ -192,7 +202,7 @@ public void GetPositionAllocationsEndingNextThreeMonthsTest() } // Check that there are no extra positions that should not be included - data.Should().HaveSameCount(shouldBeIncludedInReport, $"Exactly these positions should be included in the report, {string.Join(", ", shouldBeIncludedInReport)}, these should not be included {string.Join(", ", positionsToTest.Select(p => p.Name).Except(shouldBeIncludedInReport))}"); + data.Should().HaveSameCount(shouldBeIncludedInReport, $"Exactly these positions should be included in the report: {string.Join(", ", shouldBeIncludedInReport)}. These should not be included: {string.Join(", ", data.Select(p => p.Position.Name).Where(p => !shouldBeIncludedInReport.Contains(p)))}"); } [Fact] @@ -278,7 +288,7 @@ public void GetTBNPositionsStartingWithinThreeMonthsTests() // Check that there are no extra positions that should not be included data.Should().HaveSameCount(testData.ShouldBeIncludedInReport, $"Exactly these positions should be included in the report, {string.Join(", ", testData.ShouldBeIncludedInReport)}," + - $" these should not be included {string.Join(", ", testData.PositionsToTest.Select(p => p.Name).Except(testData.ShouldBeIncludedInReport))}"); + $" these should not be included {string.Join(", ", data.Select(p => p.Position.Name).Where(p => !testData.ShouldBeIncludedInReport.Contains(p)))}"); } From 7dfb9c15888ba305e72936580bad72eb5d3d82b9 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:57:50 +0100 Subject: [PATCH 5/9] Redid calculation --- .../WeeklyTaskOwnerReportDataCreator.cs | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs index 8d2b276cf..39af4e06c 100644 --- a/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs +++ b/src/Fusion.Summary.Functions/ReportCreator/WeeklyTaskOwnerReportDataCreator.cs @@ -63,12 +63,13 @@ public static List GetPositionAllocationsEndingNextThreeMonths var instancesWithinPeriod = FindInstancesWithinPeriod(position, new DateTimeRange() { Start = nowDate, Stop = expiringDate }); - if (instancesWithinPeriod.EndingWithinPeriod.Count == 0 - && instancesWithinPeriod.ContainedWithinPeriod.Count == 0 - && instancesWithinPeriod.StartingWithinPeriod.Any()) - { + var anyAllocatedWithinPeriod = instancesWithinPeriod.EndingWithinPeriod + .Concat(instancesWithinPeriod.ContainedWithinPeriod) + .Any(i => i.AssignedPerson is not null); + + if (!anyAllocatedWithinPeriod) continue; - } + foreach (var instance in instancesWithinPeriod.Instances) { @@ -125,10 +126,17 @@ private static InstancesWithinPeriod FindInstancesWithinPeriod(ApiPositionV2 pos private class InstancesWithinPeriod { + /// Last instance starting after period (outside of scope) public ApiPositionInstanceV2? LastInstance { get; set; } + + /// Starts outside of period and ends within period public required List EndingWithinPeriod { get; set; } + + /// Starts and ends within period public required List ContainedWithinPeriod { get; set; } - public required List StartingWithinPeriod { get; set; } // Starting within period and ending after period + + /// Starting within period and ending after period + public required List StartingWithinPeriod { get; set; } public List Instances => EndingWithinPeriod.Concat(ContainedWithinPeriod).Concat(StartingWithinPeriod).Distinct().ToList(); From 8705030f94f9bcd4060e8b9d253120eab1595a18 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:37:38 +0100 Subject: [PATCH 6/9] Move go to url to above list but bellow title and subtitle --- .../CardBuilder/AdaptiveCardBuilder.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs index bdc9b28f9..ef8079bf8 100644 --- a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs +++ b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs @@ -149,6 +149,19 @@ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnum listContainer.Items.Add(header); listContainer.Items.Add(subtitle); + + if (goToAction != null) + { + var actionSet = new AdaptiveActionSet(); + var action = new AdaptiveOpenUrlAction() + { + Title = goToAction.Title, + Url = new Uri(goToAction.Url) + }; + actionSet.Actions.Add(action); + listContainer.Items.Add(actionSet); + } + listContainer.Items.Add(grid); // If no data is present, add a "None" text @@ -162,18 +175,6 @@ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnum }); } - if (goToAction != null) - { - var actionSet = new AdaptiveActionSet(); - var action = new AdaptiveOpenUrlAction() - { - Title = goToAction.Title, - Url = new Uri(goToAction.Url) - }; - - actionSet.Actions.Add(action); - listContainer.Items.Add(actionSet); - } _adaptiveCard.Body.Add(listContainer); return this; From 8385e9b63a647dac9d79d99c8a9aa2420fbdb792 Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:06:59 +0100 Subject: [PATCH 7/9] Order entries by date --- .../TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs index fd30c98c5..b77ddb4fa 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportWorker.cs @@ -84,21 +84,21 @@ private async Task CreateAndStoreReportAsync(WeeklyTaskOwnerReportMessage messag AzureUniqueId = ea.AzureUniqueId, FullName = ea.FullName, Expires = ea.ValidTo - }).ToArray(), + }).OrderBy(ea => ea.Expires).ToArray(), PositionAllocationsEndingInNextThreeMonths = expiringPositionAllocations.Select(ep => new ApiPositionAllocationEnding() { PositionName = ep.Position.BasePosition.Name ?? string.Empty, PositionNameDetailed = ep.Position.Name, PositionExternalId = ep.Position.ExternalId ?? string.Empty, PositionAppliesTo = ep.ExpiresAt - }).ToArray(), + }).OrderBy(ep => ep.PositionAppliesTo).ToArray(), TBNPositionsStartingInLessThanThreeMonths = tbnPositions.Select(tp => new ApiTBNPositionStartingSoon() { PositionName = tp.Position.BasePosition.Name ?? string.Empty, PositionNameDetailed = tp.Position.Name, PositionExternalId = tp.Position.ExternalId ?? string.Empty, PositionAppliesFrom = tp.StartsAt - }).ToArray() + }).OrderBy(tp => tp.PositionAppliesFrom).ToArray() }; From d3a69e672c103cb083a8d542684b8039e34b772d Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:37:53 +0100 Subject: [PATCH 8/9] Allow specifying who will receive report --- .../TaskOwnerReports/WeeklyTaskOwnerReportSender.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs index 16541c9cc..948918944 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs @@ -27,6 +27,7 @@ public class WeeklyTaskOwnerReportSender private readonly AdaptiveCardRenderer cardHtmlRenderer; private readonly bool sendingNotificationEnabled = true; // Default to true so that we don't accidentally disable sending notifications private readonly string fusionUri; + private readonly string[]? filterToRecipients; private const string IsSendingNotificationEnabledKey = "WeeklyTaskOwnerReport_IsSendingNotificationEnabled"; private const string FunctionName = "weekly-task-owner-report-sender"; @@ -46,6 +47,12 @@ public WeeklyTaskOwnerReportSender(ILogger logger, sendingNotificationEnabled = enabled == 1; else if (bool.TryParse(configuration[IsSendingNotificationEnabledKey], out var enabledBool)) sendingNotificationEnabled = enabledBool; + + // For testing purposes, we can filter the recipients to only send to a specific set of people + filterToRecipients = configuration["WeeklyTaskOwnerReport_FilterToRecipientEmails"]? + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(email => email.Contains('@')) + .ToArray(); } [FunctionName(FunctionName)] @@ -56,6 +63,9 @@ public async Task RunAsync([TimerTrigger("0 0 5 * * MON", RunOnStartup = false)] if (!sendingNotificationEnabled) logger.LogInformation("Sending of notifications is disabled"); + if (filterToRecipients is { Length: > 0 }) + logger.LogInformation("Filtering to recipients: {Recipients}", string.Join(',', filterToRecipients)); + var projects = await GetProjectsInformationAsync(cancellationToken); var taskOwnerReports = await GetTaskOwnerReportsAsync(projects, cancellationToken); @@ -172,6 +182,9 @@ private async Task> CreateMailRequestsAsync(I { // TODO: Email resolution should be done before the az func sender runs, and the resolved emails should be stored on the report/project recipientEmails = await ResolveEmailsAsync(project.Recipients, cancellationToken); + + if (filterToRecipients is { Length: > 0 }) + recipientEmails = recipientEmails.Intersect(filterToRecipients, StringComparer.OrdinalIgnoreCase).ToArray(); } catch (Exception e) { From 1947d2a3ec12730cfb925cc6984ca21b7482de2a Mon Sep 17 00:00:00 2001 From: Jonathan Idland Olsnes <73334350+Jonathanio123@users.noreply.github.com> Date: Fri, 20 Dec 2024 13:19:24 +0100 Subject: [PATCH 9/9] Show max 10 items for each category in report --- .../CardBuilder/AdaptiveCardBuilder.cs | 24 ++- .../WeeklyTaskOwnerReportSender.cs | 153 +++++++++--------- 2 files changed, 101 insertions(+), 76 deletions(-) diff --git a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs index ef8079bf8..5f8244daa 100644 --- a/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs +++ b/src/Fusion.Summary.Functions/CardBuilder/AdaptiveCardBuilder.cs @@ -65,7 +65,7 @@ public AdaptiveCardBuilder AddTextRow(string valueText, string headerText, strin } - public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnumerable columnsEnumerable, GoToAction? goToAction = null) + public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnumerable columnsEnumerable, GoToAction? goToAction = null, int? maxItems = 10) { var columns = columnsEnumerable.ToList(); var listContainer = new AdaptiveContainer @@ -91,13 +91,23 @@ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnum var grid = new AdaptiveColumnSet(); + var maxItemsReached = false; + var totalRows = columns.FirstOrDefault()?.Cells.Count(c => !c.IsHeader) ?? 0; foreach (var column in columns) { + maxItemsReached = false; + var cellCount = 0; var rows = new List(); foreach (var gridCell in column.Cells) { + if (maxItems.HasValue && cellCount >= maxItems) + { + maxItemsReached = true; + break; + } + var cell = new AdaptiveTextBlock { Text = gridCell.Value, @@ -108,6 +118,8 @@ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnum }; rows.Add(cell); + if (!gridCell.IsHeader) + cellCount++; } var gridColumn = new AdaptiveColumn @@ -175,6 +187,16 @@ public AdaptiveCardBuilder AddGrid(string headerText, string subtitleText, IEnum }); } + if (maxItemsReached && maxItems.HasValue) + { + listContainer.Items.Add(new AdaptiveTextBlock + { + Text = $"And {totalRows - maxItems} more...", + Wrap = true, + HorizontalAlignment = AdaptiveHorizontalAlignment.Center + }); + } + _adaptiveCard.Body.Add(listContainer); return this; diff --git a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs index 948918944..91600d900 100644 --- a/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs +++ b/src/Fusion.Summary.Functions/Functions/TaskOwnerReports/WeeklyTaskOwnerReportSender.cs @@ -250,87 +250,90 @@ private SendEmailWithTemplateRequest CreateReportMail(string[] recipients, Proje Title = "Go to open requests", Url = $"{fusionUri}/apps/org-admin/{contextId}/open-requests?filter=awaiting-task-owner" }) - .AddGrid("Admin access expiring in less than 3 months", "(Consider extending the access in Access control management)", new List() - { - new() + .AddGrid($"Admin access expiring in less than 3 months", + "(Consider extending the access in Access control management)", new List() { - Width = AdaptiveColumnWidth.Stretch, - Cells = - [ - new GridCell(isHeader: true, value: "Name"), - ..report.AdminAccessExpiringInLessThanThreeMonths.Select(a - => new GridCell(isHeader: false, value: a.FullName)) - ] - }, - new() + new() + { + Width = AdaptiveColumnWidth.Stretch, + Cells = + [ + new GridCell(isHeader: true, value: "Name"), + ..report.AdminAccessExpiringInLessThanThreeMonths.Select(a + => new GridCell(isHeader: false, value: a.FullName)) + ] + }, + new() + { + Width = AdaptiveColumnWidth.Auto, + Cells = + [ + new GridCell(isHeader: true, value: "Expires"), + ..report.AdminAccessExpiringInLessThanThreeMonths.Select(a + => new GridCell(isHeader: false, value: a.Expires.ToString("dd/MM/yyyy"))) + ] + } + }, new GoToAction() { - Width = AdaptiveColumnWidth.Auto, - Cells = - [ - new GridCell(isHeader: true, value: "Expires"), - ..report.AdminAccessExpiringInLessThanThreeMonths.Select(a - => new GridCell(isHeader: false, value: a.Expires.ToString("dd/MM/yyyy"))) - ] - } - }, new GoToAction() - { - Title = "Go to access control management", - Url = $"{fusionUri}/apps/org-admin/{contextId}/access-control" - }) - .AddGrid("Allocations expiring next 3 months", "(Contact the resource owner if there is a need to extend the allocation)", new List() - { - new() + Title = "Go to access control management", + Url = $"{fusionUri}/apps/org-admin/{contextId}/access-control" + }) + .AddGrid($"Allocations expiring next 3 months", + "(Contact the resource owner if there is a need to extend the allocation)", new List() { - Width = AdaptiveColumnWidth.Stretch, - Cells = - [ - new GridCell(isHeader: true, value: "Position"), - ..report.PositionAllocationsEndingInNextThreeMonths.Select(p - => new GridCell(isHeader: false, value: $"{p.PositionExternalId} {p.PositionNameDetailed}")) - ] - }, - new() + new() + { + Width = AdaptiveColumnWidth.Stretch, + Cells = + [ + new GridCell(isHeader: true, value: "Position"), + ..report.PositionAllocationsEndingInNextThreeMonths.Select(p + => new GridCell(isHeader: false, value: $"{p.PositionExternalId} {p.PositionNameDetailed}")) + ] + }, + new() + { + Width = AdaptiveColumnWidth.Auto, + Cells = + [ + new GridCell(isHeader: true, value: "End date"), + ..report.PositionAllocationsEndingInNextThreeMonths.Select(p + => new GridCell(isHeader: false, value: p.PositionAppliesTo.ToString("dd/MM/yyyy"))) + ] + } + }, new GoToAction() { - Width = AdaptiveColumnWidth.Auto, - Cells = - [ - new GridCell(isHeader: true, value: "End date"), - ..report.PositionAllocationsEndingInNextThreeMonths.Select(p - => new GridCell(isHeader: false, value: p.PositionAppliesTo.ToString("dd/MM/yyyy"))) - ] - } - }, new GoToAction() - { - Title = "Go to positions listing view", - Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view?filter=allocations-exp-3m" - }) - .AddGrid("TBN positions with start date next 3 months", "(Please create a resource request or update the position start-date)", new List() - { - new() + Title = "Go to positions listing view", + Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/listing-view?filter=allocations-exp-3m" + }) + .AddGrid($"TBN positions with start date next 3 months", + "(Please create a resource request or update the position start-date)", new List() { - Width = AdaptiveColumnWidth.Stretch, - Cells = - [ - new GridCell(isHeader: true, value: "Position"), - ..report.TBNPositionsStartingInLessThanThreeMonths.Select(p - => new GridCell(isHeader: false, value: $"{p.PositionExternalId} {p.PositionNameDetailed}")) - ] - }, - new() + new() + { + Width = AdaptiveColumnWidth.Stretch, + Cells = + [ + new GridCell(isHeader: true, value: "Position"), + ..report.TBNPositionsStartingInLessThanThreeMonths.Select(p + => new GridCell(isHeader: false, value: $"{p.PositionExternalId} {p.PositionNameDetailed}")) + ] + }, + new() + { + Width = AdaptiveColumnWidth.Auto, + Cells = + [ + new GridCell(isHeader: true, value: "Start date"), + ..report.TBNPositionsStartingInLessThanThreeMonths.Select(p + => new GridCell(isHeader: false, value: p.PositionAppliesFrom.ToString("dd/MM/yyyy"))) + ] + } + }, new GoToAction() { - Width = AdaptiveColumnWidth.Auto, - Cells = - [ - new GridCell(isHeader: true, value: "Start date"), - ..report.TBNPositionsStartingInLessThanThreeMonths.Select(p - => new GridCell(isHeader: false, value: p.PositionAppliesFrom.ToString("dd/MM/yyyy"))) - ] - } - }, new GoToAction() - { - Title = "Go to positions timeline view", - Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/timeline-view?filter=tbn-pos-3m" - }) + Title = "Go to positions timeline view", + Url = $"{fusionUri}/apps/org-admin/{contextId}/edit-positions/timeline-view?filter=tbn-pos-3m" + }) .Build(); var subject = $"Weekly summary - {project.Name}";