From 5628120deddd3cf3fc1f9b462ba5ee1f53fb4ee9 Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Thu, 30 Nov 2023 08:30:56 +0100 Subject: [PATCH 01/13] Added a AdaptiveCardsBuilder and changed the way we generate adaptivecards --- .../AdaptiveCards/AdaptiveCardBuilder.cs | 183 +++++++++++++++ .../ResourceOwnerAdaptiveCardData.cs | 2 +- .../ScheduledReportContentBuilderFunction.cs | 220 ++---------------- 3 files changed, 207 insertions(+), 198 deletions(-) create mode 100644 src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs rename src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/{AdaptiveCard => AdaptiveCards}/ResourceOwnerAdaptiveCardData.cs (97%) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs new file mode 100644 index 000000000..e3c631837 --- /dev/null +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs @@ -0,0 +1,183 @@ +using AdaptiveCards; +using System; +using System.Collections.Generic; +using System.Linq; +using static Fusion.Resources.Functions.ApiClients.IResourcesApiClient; + +namespace Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards +{ + public class AdaptiveCardBuilder + { + private AdaptiveCard _adaptiveCard; + + + public AdaptiveCardBuilder() + { + _adaptiveCard = new AdaptiveCard(new AdaptiveSchemaVersion(1, 0)); // Setting the version of AdaptiveCards + } + public AdaptiveCardBuilder AddHeading(string text) + { + var heading = new AdaptiveTextBlock + { + Text = text, + Wrap = true, + HorizontalAlignment = AdaptiveHorizontalAlignment.Center, + Separator = true, + Size = AdaptiveTextSize.Large, + Weight = AdaptiveTextWeight.Bolder, + }; + _adaptiveCard.Body.Add(heading); + return this; + } + + public AdaptiveCardBuilder AddColumnSet(params AdaptiveCardColumn[] columns) + { + var columnSet = new AdaptiveColumnSet + { + Columns = columns.Select(col => col.Column).ToList(), + Separator = true + }; + _adaptiveCard.Body.Add(columnSet); + return this; + } + + public AdaptiveCardBuilder AddListContainer(string headerText, IEnumerable items, string text1, string text2) + { + var listContainer = new AdaptiveContainer + { + Separator = true, + Items = new List + { + new AdaptiveTextBlock + { + Weight = AdaptiveTextWeight.Bolder, + Text = headerText, + Wrap = true, + Size = AdaptiveTextSize.Large, + + }, + new AdaptiveColumnSet + { + Columns = new List + { + new AdaptiveColumn + { + Width = AdaptiveColumnWidth.Stretch, + Items = new List + { + new AdaptiveCardList(items, text1, text2).List + } + } + } + } + } + }; + _adaptiveCard.Body.Add(listContainer); + return this; + } + + public AdaptiveCard Build() + { + return _adaptiveCard; + } + + public class AdaptiveCardColumn + { + public AdaptiveColumn Column { get; } + + public AdaptiveCardColumn(string numberText, string headerText, string? customText = null) + { + Column = new AdaptiveColumn + { + Width = AdaptiveColumnWidth.Stretch, + Separator = true, + Spacing = AdaptiveSpacing.Medium, + Items = new List + { + new AdaptiveTextBlock + { + Text = $"{numberText} - {customText ?? ""}", + Wrap = true, + HorizontalAlignment = AdaptiveHorizontalAlignment.Center, + Size = AdaptiveTextSize.ExtraLarge + }, + new AdaptiveTextBlock + { + Text = headerText, + Wrap = true, + HorizontalAlignment = AdaptiveHorizontalAlignment.Center + } + } + }; + } + } + + public class AdaptiveCardList + { + public AdaptiveContainer List { get; } + + public AdaptiveCardList(IEnumerable items, string titlePropertyName, string valuePropertyName) + { + var listItems = new List(); + + foreach (var item in items) + { + var value1 = typeof(PersonnelContent).GetProperty(titlePropertyName)?.GetValue(item)?.ToString(); + var value2 = typeof(PersonnelContent).GetProperty(valuePropertyName)?.GetValue(item); + var value2Text = ""; + + + //FIXME: This is not a good way to differ between the two types of lists + if (valuePropertyName != "TotalWorkload") + { + var dateValue = typeof(PersonnelPosition).GetProperty("AppliesTo")?.GetValue(value2); + var dateText = dateValue is DateTime dateTime ? dateTime.ToString("dd/MM/yyyy") : string.Empty; + value2Text = $"End date: {dateText}"; + } + else + { + value2Text = value2.ToString() + "%"; + } + + + if (!string.IsNullOrEmpty(value1) && value2 != null) + { + + var columnSet = new AdaptiveColumnSet + { + Columns = new List + { + new AdaptiveColumn + { + Width = AdaptiveColumnWidth.Stretch, + Items = new List + { + new AdaptiveTextBlock + { Text = $"{value1} ", Wrap = true, HorizontalAlignment = AdaptiveHorizontalAlignment.Left }, + } + }, + new AdaptiveColumn + { + Width = AdaptiveColumnWidth.Stretch, + Items = new List + { + new AdaptiveTextBlock + { Text = value2Text, Wrap = true, HorizontalAlignment = AdaptiveHorizontalAlignment.Right }, + } + } + } + }; + listItems.Add(columnSet); + + } + + } + + List = new AdaptiveContainer + { + Items = listItems + }; + } + } + } +} diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCard/ResourceOwnerAdaptiveCardData.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs similarity index 97% rename from src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCard/ResourceOwnerAdaptiveCardData.cs rename to src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs index 13abcefda..e22111d66 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCard/ResourceOwnerAdaptiveCardData.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -namespace Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCard; +namespace Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards; public class ResourceOwnerAdaptiveCardData { diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 517d4a000..b99cdb4fa 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -8,14 +8,14 @@ using Fusion.Resources.Functions.ApiClients; using Fusion.Resources.Functions.ApiClients.ApiModels; using Fusion.Resources.Functions.Functions.Notifications.Models; -using Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCard; +using Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards; using Fusion.Resources.Functions.Functions.Notifications.Models.DTOs; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.ServiceBus; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using static Fusion.Resources.Functions.ApiClients.IResourcesApiClient; +using static Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards.AdaptiveCardBuilder; namespace Fusion.Resources.Functions.Functions.Notifications; @@ -143,17 +143,17 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD var card = ResourceOwnerAdaptiveCardBuilder(new ResourceOwnerAdaptiveCardData - { - NumberOfOlderRequests = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, - NumberOfOpenRequests = totalNumberOfOpenRequests, - NumberOfNewRequestsWithNoNomination = + { + NumberOfOlderRequests = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, + NumberOfOpenRequests = totalNumberOfOpenRequests, + NumberOfNewRequestsWithNoNomination = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, - NumberOfExtContractsEnding = 0, // TODO: Work in progress... - PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent, - PercentAllocationOfTotalCapacity = percentageOfTotalCapacity, - TotalNumberOfRequests = totalNumberOfRequests, - PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, - }, + NumberOfExtContractsEnding = 0, // TODO: Work in progress... + PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent, + PercentAllocationOfTotalCapacity = percentageOfTotalCapacity, + TotalNumberOfRequests = totalNumberOfRequests, + PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, + }, fullDepartment); var sendNotification = await _notificationsClient.SendNotification( @@ -235,192 +235,18 @@ private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson p private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, string departmentIdentifier) { - var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 2)); - - card.Body.Add(new AdaptiveTextBlock - { - Text = $"**Weekly summary - {departmentIdentifier}**", - Size = AdaptiveTextSize.Large, - Weight = AdaptiveTextWeight.Bolder, - Wrap = true // Allow text to wrap - }); - card = CreateAdaptiveCardTemp(card, cardData); + var card = new AdaptiveCardBuilder() + .AddHeading($"**Weekly summary - {departmentIdentifier}**") + .AddColumnSet(new AdaptiveCardColumn(cardData.PercentAllocationOfTotalCapacity.ToString(), "Capacity in use", "%")) + .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfRequests.ToString(), "Total requests")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfOpenRequests.ToString(), "Open requests")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfNewRequestsWithNoNomination.ToString(), "Requests with start date less than 3 months")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfOlderRequests.ToString(), "Requests with start date more than 3 months")) + .AddListContainer("Positions ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") + .AddListContainer("Personnel with more than 100% FTE:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") + .Build(); return card; - } - - - // FIXME: Temporary way to compose a adaptive card. Needs refactoring - private static AdaptiveCard CreateAdaptiveCardTemp(AdaptiveCard adaptiveCard, - ResourceOwnerAdaptiveCardData cardData) - { - adaptiveCard = AddColumnsAndTextToAdaptiveCard(adaptiveCard, "Capacity in use", "%", - cardData.PercentAllocationOfTotalCapacity.ToString()); - adaptiveCard = AddColumnsAndTextToAdaptiveCard(adaptiveCard, "Total requests", "", - cardData.TotalNumberOfRequests.ToString()); - adaptiveCard = AddColumnsAndTextToAdaptiveCard(adaptiveCard, "Open requests", "", - cardData.NumberOfOpenRequests.ToString()); - adaptiveCard = AddColumnsAndTextToAdaptiveCard(adaptiveCard, "Requests with start date less than 3 months", "", - cardData.NumberOfNewRequestsWithNoNomination.ToString()); - adaptiveCard = AddColumnsAndTextToAdaptiveCard(adaptiveCard, "Requests with start date more than 3 months", "", - cardData.NumberOfOlderRequests.ToString()); - adaptiveCard = AddColumnsAndTextToAdaptiveCardForAllocationWithNoFutureAllocations(adaptiveCard, - "Positions ending soon with no future allocation", "End date: ", cardData); - adaptiveCard = AddColumnsAndTextToAdaptiveCardForPersonnelWithMoreThan100PercentFTE(adaptiveCard, - "Personnel with more than 100% FTE:", "% FTE", cardData); - - return adaptiveCard; - } - - private static AdaptiveCard AddColumnsAndTextToAdaptiveCard(AdaptiveCard adaptiveCard, string customtext1, - string? customtext2, string value) - { - var container = new AdaptiveContainer(); - container.Separator = true; - var columnSet1 = new AdaptiveColumnSet(); - - var column1 = new AdaptiveColumn(); - column1.Width = AdaptiveColumnWidth.Stretch; - column1.Separator = true; - column1.Spacing = AdaptiveSpacing.Medium; - - var textBlock1 = new AdaptiveTextBlock(); - textBlock1.Text = customtext1; - textBlock1.Wrap = true; - textBlock1.HorizontalAlignment = AdaptiveHorizontalAlignment.Center; - - var textBlock2 = new AdaptiveTextBlock() - { - Wrap = true, - Text = value + customtext2, - Size = AdaptiveTextSize.ExtraLarge, - HorizontalAlignment = AdaptiveHorizontalAlignment.Center, - }; - - column1.Add(textBlock2); - column1.Add(textBlock1); - columnSet1.Add(column1); - container.Add(columnSet1); - adaptiveCard.Body.Add(container); - - - return adaptiveCard; - } - - private static AdaptiveCard AddColumnsAndTextToAdaptiveCardForAllocationWithNoFutureAllocations( - AdaptiveCard adaptiveCard, string customtext1, string customtext2, ResourceOwnerAdaptiveCardData carddata) - { - var container = new AdaptiveContainer() - { Separator = true }; - - var columnSet1 = new AdaptiveColumnSet(); - - var column1 = new AdaptiveColumn(); - var textBlock1 = new AdaptiveTextBlock() - { - Text = customtext1, - Wrap = true, - Size = AdaptiveTextSize.Default, - Weight = AdaptiveTextWeight.Bolder - }; - column1.Add(textBlock1); - - foreach (var p in carddata.PersonnelPositionsEndingWithNoFutureAllocation) - { - var columnset2 = new AdaptiveColumnSet(); - - var column2 = new AdaptiveColumn(); - var textBlock2 = new AdaptiveTextBlock() - { - Text = p.FullName, - Wrap = true, - Size = AdaptiveTextSize.Default, - HorizontalAlignment = AdaptiveHorizontalAlignment.Left - }; - - - column2.Add(textBlock2); - columnset2.Add(column2); - - var column3 = new AdaptiveColumn(); - var textBlock3 = new AdaptiveTextBlock() - { - Text = customtext2 + p.EndingPosition.AppliesTo.Value.Date.ToShortDateString(), - Wrap = true, - Size = AdaptiveTextSize.Default, - HorizontalAlignment = AdaptiveHorizontalAlignment.Right - }; - - column3.Add(textBlock3); - columnset2.Add(column3); - column1.Add(columnset2); - } - - - columnSet1.Add(column1); - container.Add(columnSet1); - - adaptiveCard.Body.Add(container); - - return adaptiveCard; - } - - private static AdaptiveCard AddColumnsAndTextToAdaptiveCardForPersonnelWithMoreThan100PercentFTE( - AdaptiveCard adaptiveCard, string customtext1, string customtext2, ResourceOwnerAdaptiveCardData carddata) - { - var container = new AdaptiveContainer() - { Separator = true }; - - var columnSet1 = new AdaptiveColumnSet(); - - var column1 = new AdaptiveColumn(); - var textBlock1 = new AdaptiveTextBlock() - { - Text = customtext1, - Wrap = true, - Size = AdaptiveTextSize.Default, - Weight = AdaptiveTextWeight.Bolder - }; - column1.Add(textBlock1); - - foreach (var p in carddata.PersonnelAllocatedMoreThan100Percent) - { - var columnset2 = new AdaptiveColumnSet(); - - var column2 = new AdaptiveColumn(); - var textBlock2 = new AdaptiveTextBlock() - { - Text = p.FullName, - Wrap = true, - Size = AdaptiveTextSize.Default, - HorizontalAlignment = AdaptiveHorizontalAlignment.Left - }; - - - column2.Add(textBlock2); - columnset2.Add(column2); - - var column3 = new AdaptiveColumn(); - var textBlock3 = new AdaptiveTextBlock() - { - Text = p.TotalWorkload + customtext2, - Wrap = true, - Size = AdaptiveTextSize.Default, - HorizontalAlignment = AdaptiveHorizontalAlignment.Right - }; - - column3.Add(textBlock3); - columnset2.Add(column3); - column1.Add(columnset2); - } - - - columnSet1.Add(column1); - container.Add(columnSet1); - - adaptiveCard.Body.Add(container); - - return adaptiveCard; - } + } } \ No newline at end of file From e214ba5833d2a216835e03feaad9d04d958fd9d7 Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Thu, 30 Nov 2023 14:44:13 +0100 Subject: [PATCH 02/13] Adjustments to getting data for ReportNotification for ResourceOwner --- .../ApiClients/IResourcesApiClient.cs | 43 ++++- .../ApiClients/ResourcesApiClient.cs | 8 + .../AdaptiveCards/AdaptiveCardBuilder.cs | 14 +- .../ResourceOwnerAdaptiveCardData.cs | 14 +- .../ScheduledReportContentBuilderFunction.cs | 158 +++++++++++++----- 5 files changed, 181 insertions(+), 56 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs index 14857e765..a1f0ce145 100644 --- a/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs @@ -14,6 +14,7 @@ public interface IResourcesApiClient Task ReassignRequestAsync(ResourceAllocationRequest item, string? department); Task> GetAllRequestsForDepartment(string departmentIdentifier); Task> GetAllPersonnelForDepartment(string departmentIdentifier); + Task> GetLeaveForPersonnel(string personId); #region Models @@ -30,6 +31,13 @@ public class ResourceAllocationRequest public bool HasProposedPerson => ProposedPerson?.Person.AzureUniquePersonId is not null; public string? State { get; set; } public Workflow? workflow { get; set; } + public DateTimeOffset Created { get; set; } + public InternalPersonnelPerson? CreatedBy { get; set; } + + public DateTimeOffset? Updated { get; set; } + public InternalPersonnelPerson? UpdatedBy { get; set; } + public DateTimeOffset? LastActivity { get; set; } + public bool IsDraft { get; set; } } public enum RequestState @@ -42,7 +50,7 @@ public class Workflow public string LogicAppName { get; set; } public string LogicAppVersion { get; set; } - [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public ApiWorkflowState State { get; set; } public IEnumerable Steps { get; set; } @@ -61,7 +69,7 @@ public class WorkflowStep /// /// Pending, Approved, Rejected, Skipped /// - [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public ApiWorkflowStepState State { get; set; } public DateTimeOffset? Started { get; set; } @@ -79,9 +87,8 @@ public enum ApiWorkflowStepState { Pending, Approved, Rejected, Skipped, Unknown public class ProposedPerson { - public Person Person { get; set; } = null!; + public InternalPersonnelPerson Person { get; set; } = null!; } - public class Person { public Guid? AzureUniquePersonId { get; set; } @@ -98,7 +105,6 @@ public class ProjectReference public class InternalPersonnelPerson { - public Guid? AzureUniquePersonId { get; set; } public string? Mail { get; set; } = null!; public string? Name { get; set; } = null!; @@ -110,6 +116,7 @@ public class InternalPersonnelPerson public bool IsResourceOwner { get; set; } public string? AccountType { get; set; } public List PositionInstances { get; set; } = new List(); + public List ApiPersonAbsences { get; set; } = new List(); } public class PersonnelPosition @@ -127,6 +134,32 @@ public class PersonnelPosition public double Workload { get; set; } public ProjectReference? Project { get; set; } } + + public class ApiPersonAbsence + { + public Guid Id { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public DateTimeOffset? Created { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public InternalPersonnelPerson? CreatedBy { get; set; } + public bool IsPrivate { get; set; } + public string? Comment { get; set; } + //public ApiTaskDetails? TaskDetails { get; set; } // Trengs denne? + public DateTimeOffset? AppliesFrom { get; set; } + public DateTimeOffset? AppliesTo { get; set; } + public ApiAbsenceType? Type { get; set; } + public double? AbsencePercentage { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool IsActive => AppliesFrom <= DateTime.UtcNow.Date && AppliesTo >= DateTime.UtcNow.Date; + + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum ApiAbsenceType { Absence, Vacation, OtherTasks } + + + #endregion Models } } \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/ResourcesApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/ResourcesApiClient.cs index 368ea2240..2f130218d 100644 --- a/src/backend/function/Fusion.Resources.Functions/ApiClients/ResourcesApiClient.cs +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/ResourcesApiClient.cs @@ -55,6 +55,14 @@ public async Task> GetAllPersonnelForDepart return response.Value.ToList(); } + public async Task> GetLeaveForPersonnel(string personId) + { + var response = await resourcesClient.GetAsJsonAsync>( + $"persons/{personId}/absence"); + + return response.Value.ToList(); + } + public async Task ReassignRequestAsync(ResourceAllocationRequest item, string? department) { var content = JsonConvert.SerializeObject(new { AssignedDepartment = department }); diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs index e3c631837..fd3eae07b 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs @@ -76,6 +76,18 @@ public AdaptiveCardBuilder AddListContainer(string headerText, IEnumerable PersonnelPositionsEndingWithNoFutureAllocation { get; set; } - public int PercentAllocationOfTotalCapacity { get; set; } + internal IEnumerable PersonnelAllocatedMoreThan100Percent { get; set; } - public int NumberOfExtContractsEnding { get; set; } } \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index b99cdb4fa..c105361e0 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -90,69 +90,104 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD { var threeMonthsFuture = DateTime.UtcNow.AddMonths(3); var today = DateTime.UtcNow; + var sevenDaysAgo = DateTime.UtcNow.AddDays(-7); // Get all requests for specific Department regardsless of state var departmentRequests = await _resourceClient.GetAllRequestsForDepartment(fullDepartment); + // Get all the personnel for the specific department + var personnelForDepartment = await _resourceClient.GetAllPersonnelForDepartment(fullDepartment); + personnelForDepartment = await GetPersonnelLeave(personnelForDepartment); + + + + //1.Number of personnel: + //number of personnel in the department(one specific org unit) + // Hvordan finne? + // Hent ut alle ressursene for en gitt avdeling + // OK? + var numberOfPersonnel = personnelForDepartment.Count(); + + //2.Capacity in use: + //capacity in use by %. + //Calculated by total current workload for all personnel / (100 % workload x number of personnel - (total % leave)), + //a.e.g. 10 people in department: 800 % current workload / (1000 % -120 % leave) = 91 % capacity in use + // OK - Inkluderer nå leave.. + var percentageOfTotalCapacity = FindTotalCapacityIncludingLeave(personnelForDepartment.ToList()); + + + // 3.New requests last week: + // number of requests received last 7 days + // Notat: En request kan ha blitt opprettet for 7 dager siden, men ikke oversendt til ressurseiere - Det kan være + // Inkluderer foreløpig alle requestene uavhengig av hvilken state de er + var numberOfRequestsLastWeek = departmentRequests.Where(req => req.Created > sevenDaysAgo && !req.IsDraft).Count(); - // Count all of the number of requests sent to the department. We may change this to only include a specific timeframe in the future (last 12 months) - // 1. Total number of request sent to department - var totalNumberOfRequests = departmentRequests.Count(); - // Filter to only include the ones that have start-date in more than 3 months AND state not completed - // 2. Number of request that have more than 3 months to start data(link to system with filtered view) - var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests - .Count(x => !x.State.Contains(RequestState.completed.ToString()) && - x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); + //4.Open request: + //number of requests with no proposed candidate + // Only to include those requests which have state approval (this means that the resource owner needs to process the requests in some way) + // OK? - Må sjekkes og finne noen som har approval... + var totalNumberOfOpenRequests = departmentRequests.Count(req => !req.HasProposedPerson && !req.State.Contains(RequestState.completed.ToString())); + + + //5.Requests with start-date < 3 months: + //number of requests with start date within less than 3 months // Filter to only inlclude the ones that have start-date in less than 3 months and start-date after today and is not complete and has no proposedPerson assigned to them - // 3. Number of requests that are less than 3 month to start data with no nomination. var numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination = departmentRequests .Count(x => !x.State.Contains(RequestState.completed.ToString()) && - (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && - x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); + (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && + x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); - // Only to include those requests which have state approval (this means that the resource owner needs to process the requests in some way) - // 4. Number of open requests. - var totalNumberOfOpenRequests = departmentRequests - .Count(x => !x.State.Contains(RequestState.completed.ToString())); + //6.Requests with start-date > 3 months: + //number of requests with start date later than next 3 months + // Filter to only include the ones that have start-date in more than 3 months AND state not completed + var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests + .Count(x => !x.State.Contains(RequestState.completed.ToString()) && + x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); - // Get all the personnel for the specific department - var personnelForDepartment = await _resourceClient.GetAllPersonnelForDepartment(fullDepartment); + // TODO: + //7.Average time to handle request: + //average number of days from request created/ sent to candidate is proposed - last 6 months - //5. List with personnel positions ending within 3 months and with no future allocation (link to personnel allocation) - var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); + // TODO: + //8.Allocation changes awaiting task owner action: + //number of allocation changes made by resource owner awaiting task owner action + //Må hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke på om det er gjort endringer her den siste tiden + // TODO: + //9.Project changes affecting next 3 months: + //number of project changes(changes initiated by project / task) with a change affecting the next 3 months - // 6. Number of personnel allocated more than 100 % + //10.Allocations ending soon with no future allocation: -Skal være ok ? + //list of allocations ending within next 3 months where the person allocated does not continue in the position(i.e.no future splits with the same person allocated) + var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); + + //11.Personnel with more than 100 % workload: -OK + //(as in current pilot, but remove "FTE") list of persons with total allocation > 100 %, total % workload should be visible after person name + // TODO: Fiks formatering og oppdeling av innhold her var listOfPersonnelsWithMoreThan100Percent = personnelForDepartment.Where(p => p.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum() > 100); var listOfPersonnelForDepartmentWithMoreThan100Percent = listOfPersonnelsWithMoreThan100Percent.Select(p => CreatePersonnelWithTBEContent(p)); - //7. % of total allocation vs.capacity - // Show this as a percentagenumber (in the first draft) - var percentageOfTotalCapacity = FindTotalPercentagesAllocatedOfTotal(personnelForDepartment.ToList()); - - - //8.EXT Contracts ending within 3 months ? (data to be imported from SAP or AD) - // ContractPersonnel'et? - Knyttet til projectmaster -> Knyttet til orgkart - // Skip this for now... var card = ResourceOwnerAdaptiveCardBuilder(new ResourceOwnerAdaptiveCardData { - NumberOfOlderRequests = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, + TotalNumberOfPersonnel = numberOfPersonnel, + TotalCapacityInUsePercentage = percentageOfTotalCapacity, + NumberOfRequestsLastWeek = numberOfRequestsLastWeek, NumberOfOpenRequests = totalNumberOfOpenRequests, - NumberOfNewRequestsWithNoNomination = - numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, - NumberOfExtContractsEnding = 0, // TODO: Work in progress... - PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent, - PercentAllocationOfTotalCapacity = percentageOfTotalCapacity, - TotalNumberOfRequests = totalNumberOfRequests, + NumberOfRequestsStartingInMoreThanThreeMonths = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, + NumberOfRequestsStartingInLessThanThreeMonths = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, + AverageTimeToHandleRequests = 0, + AllocationChangesAwaitingTaskOwnerAction = 0, + ProjectChangesAffectingNextThreeMonths = 0, PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, + PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent }, fullDepartment); @@ -186,18 +221,36 @@ private IEnumerable FilterPersonnelWithoutFutureAllocations( return personnelWithoutFutureAllocations.Select(p => CreatePersonnelContent(p)); } - // Without taking LEAVE into considerations - private int FindTotalPercentagesAllocatedOfTotal(List listOfInternalPersonnel) + private async Task> GetPersonnelLeave(IEnumerable listOfInternalPersonnel) + { + List newList = listOfInternalPersonnel.ToList(); + for (int i =0; i < newList.Count(); i++) + { + var absence = await _resourceClient.GetLeaveForPersonnel(newList[i].AzureUniquePersonId.ToString()); + newList[i].ApiPersonAbsences = absence.ToList(); + } + + listOfInternalPersonnel = newList; + return listOfInternalPersonnel; + } + + private int FindTotalCapacityIncludingLeave(List listOfInternalPersonnel) { + //Calculated by total current workload for all personnel / (100 % workload x number of personnel - (total % leave)), + //a.e.g. 10 people in department: 800 % current workload / (1000 % -120 % leave) = 91 % capacity in use + var totalWorkLoad = 0.0; + double? totalLeave = 0.0; + foreach (var personnel in listOfInternalPersonnel) { + totalLeave += personnel.ApiPersonAbsences.Where(ab => ab.Type == ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); totalWorkLoad += personnel.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); } - var totalPercentage = totalWorkLoad / (listOfInternalPersonnel.Count * 100) * 100; + var totalPercentageInludeLeave = totalWorkLoad / ((listOfInternalPersonnel.Count * 100) - totalLeave) * 100; - return Convert.ToInt32(totalPercentage); + return Convert.ToInt32(totalPercentageInludeLeave); } private PersonnelContent CreatePersonnelContent(InternalPersonnelPerson person) @@ -235,18 +288,33 @@ private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson p private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, string departmentIdentifier) { + // Plasser denne en annen plass + var baseUri = "https://fusion-s-portal-ci.azurewebsites.net/apps/personnel-allocation/"; + var avdelingsId = "52586050"; + var card = new AdaptiveCardBuilder() .AddHeading($"**Weekly summary - {departmentIdentifier}**") - .AddColumnSet(new AdaptiveCardColumn(cardData.PercentAllocationOfTotalCapacity.ToString(), "Capacity in use", "%")) - .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfRequests.ToString(), "Total requests")) + .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfPersonnel.ToString(), "Number of personnel")) + .AddColumnSet(new AdaptiveCardColumn(cardData.TotalCapacityInUsePercentage.ToString(), "Capacity in use", "%")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsLastWeek.ToString(), "New requests last week")) .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfOpenRequests.ToString(), "Open requests")) - .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfNewRequestsWithNoNomination.ToString(), "Requests with start date less than 3 months")) - .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfOlderRequests.ToString(), "Requests with start date more than 3 months")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInLessThanThreeMonths.ToString(), "Requests with start date < 3 months")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInMoreThanThreeMonths.ToString(), "Requests with start date > 3 months")) + + // Not finished + .AddColumnSet(new AdaptiveCardColumn(/*(cardData.AverageTimeToHandleRequests.ToString()*/ "NA", "Average time to handle request")) // WIP + .AddColumnSet(new AdaptiveCardColumn(/*cardData.AllocationChangesAwaitingTaskOwnerAction.ToString()*/ "NA", "Allocation changes awaiting task owner action")) // WIP + .AddColumnSet(new AdaptiveCardColumn(/*cardData.ProjectChangesAffectingNextThreeMonths.ToString()*/ "NA", "Project changes affecting next 3 months")) // WIP + .AddListContainer("Positions ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") - .AddListContainer("Personnel with more than 100% FTE:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") + .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") + + .AddActionButton("Go to Personnel allocation app", $"{baseUri}{avdelingsId}") + .Build(); return card; - } + } + } \ No newline at end of file From f33d72cdeb21c26ebc24c5fdd084550d0c826f8b Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Tue, 5 Dec 2023 08:47:12 +0100 Subject: [PATCH 03/13] Added BeTrustedApplication on GetPersonAbsence to be able to access from functions --- .../Controllers/Person/PersonAbsenceController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs b/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs index ce529a4f2..f1689d8b9 100644 --- a/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs +++ b/src/backend/api/Fusion.Resources.Api/Controllers/Person/PersonAbsenceController.cs @@ -54,7 +54,9 @@ public async Task>> GetPersonAbsenc r.AlwaysAccessWhen() .CurrentUserIs(profile.Identifier) .FullControl() - .FullControlInternal(); + .FullControlInternal() + .BeTrustedApplication(); + r.AnyOf(or => { From 503462159eb6040d0f7c9aeb455d71699248ca71 Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Mon, 11 Dec 2023 08:20:09 +0100 Subject: [PATCH 04/13] Update notifications: AverageTimeToHandleRequests and ChangesForResourceDepartment --- .../ApiClients/IOrgApiClient.cs | 73 +++++++++++ .../ApiClients/IResourcesApiClient.cs | 11 +- .../ApiClients/OrgApiClient.cs | 32 +++++ .../IServiceCollectionExtensions.cs | 3 + .../ResourceOwnerAdaptiveCardData.cs | 2 +- .../ScheduledReportContentBuilderFunction.cs | 124 ++++++++++++++---- 6 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs create mode 100644 src/backend/function/Fusion.Resources.Functions/ApiClients/OrgApiClient.cs diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs new file mode 100644 index 000000000..866448585 --- /dev/null +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs @@ -0,0 +1,73 @@ +using Fusion.ApiClients.Org; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Fusion.Resources.Functions.ApiClients; + +public interface IOrgClient +{ + Task GetChangeLog(string projectId, string positionId, string instanceId); +} + + +#region model +public class ApiChangeLog +{ + public Guid ProjectId { get; set; } + public DateTimeOffset? FirstEventDate { get; set; } + public DateTimeOffset? LastEventDate { get; set; } + public List Events { get; set; } + +} + +public class ApiChangeLogEvent +{ + + public Guid? PositionId { get; set; } + public string? PositionName { get; set; } + public string? PositionExternalId { get; set; } + public Guid? InstanceId { get; set; } + public string Name { get; set; } + public string ChangeCategory { get; set; } + public ApiPersonV2? Actor { get; set; } + public DateTimeOffset TimeStamp { get; set; } + public string? Description { get; set; } + public string ChangeType { get; set; } + public object? PayLoad { get; set; } + + public ApiInstanceSnapshot? Instance { get; init; } + + public Guid EventId { get; set; } + public string EventFriendlyName { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public Guid? DraftId { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? ChangeSource { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string? ChangeSourceId { get; set; } + + public class ApiInstanceSnapshot + { + public DateTime? AppliesFrom { get; set; } + public DateTime? AppliesTo { get; set; } + public double? WorkLoad { get; set; } + } +} + +public enum ChangeType +{ + PositionInstanceCreated, + PersonAssignedToPosition, + PositionInstanceAllocationStateChanged, + PositionInstanceAppliesToChanged, + PositionInstanceAppliesFromChanged, + PositionInstanceParentPositionIdChanged, + PositionInstancePercentChanged, + PositionInstanceLocationChanged +} + +#endregion \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs index a1f0ce145..ad50cdfa2 100644 --- a/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs @@ -30,10 +30,9 @@ public class ResourceAllocationRequest public ProposedPerson? ProposedPerson { get; set; } public bool HasProposedPerson => ProposedPerson?.Person.AzureUniquePersonId is not null; public string? State { get; set; } - public Workflow? workflow { get; set; } + public Workflow? Workflow { get; set; } public DateTimeOffset Created { get; set; } public InternalPersonnelPerson? CreatedBy { get; set; } - public DateTimeOffset? Updated { get; set; } public InternalPersonnelPerson? UpdatedBy { get; set; } public DateTimeOffset? LastActivity { get; set; } @@ -49,12 +48,9 @@ public class Workflow { public string LogicAppName { get; set; } public string LogicAppVersion { get; set; } - [JsonConverter(typeof(JsonStringEnumConverter))] public ApiWorkflowState State { get; set; } - public IEnumerable Steps { get; set; } - public enum ApiWorkflowState { Running, Canceled, Error, Completed, Terminated, Unknown } } @@ -63,25 +59,20 @@ public class WorkflowStep { public string Id { get; set; } public string Name { get; set; } - public bool IsCompleted => Completed.HasValue; - /// /// Pending, Approved, Rejected, Skipped /// [JsonConverter(typeof(JsonStringEnumConverter))] public ApiWorkflowStepState State { get; set; } - public DateTimeOffset? Started { get; set; } public DateTimeOffset? Completed { get; set; } public DateTimeOffset? DueDate { get; set; } public InternalPersonnelPerson? CompletedBy { get; set; } public string Description { get; set; } public string? Reason { get; set; } - public string? PreviousStep { get; set; } public string? NextStep { get; set; } - public enum ApiWorkflowStepState { Pending, Approved, Rejected, Skipped, Unknown } } diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/OrgApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/OrgApiClient.cs new file mode 100644 index 000000000..b05f683b1 --- /dev/null +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/OrgApiClient.cs @@ -0,0 +1,32 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Fusion.Resources.Functions.ApiClients.ApiModels; +using Fusion.Resources.Functions.Integration; + +namespace Fusion.Resources.Functions.ApiClients; + +public class OrgClient : IOrgClient +{ + private readonly HttpClient orgClient; + + public OrgClient(IHttpClientFactory httpClientFactory) + { + orgClient = httpClientFactory.CreateClient(HttpClientNames.Application.Org); + orgClient.Timeout = TimeSpan.FromMinutes(5); + } + + public async Task GetChangeLog(string projectId, string positionId, string instanceId) + { + var data = + await orgClient.GetAsJsonAsync($"/projects/{projectId}/positions/{positionId}/instances/{instanceId}/change-log?api-version=2.0"); + + return data; + } + + + +} \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Configuration/IServiceCollectionExtensions.cs b/src/backend/function/Fusion.Resources.Functions/Configuration/IServiceCollectionExtensions.cs index 536db641e..d4cc985de 100644 --- a/src/backend/function/Fusion.Resources.Functions/Configuration/IServiceCollectionExtensions.cs +++ b/src/backend/function/Fusion.Resources.Functions/Configuration/IServiceCollectionExtensions.cs @@ -53,6 +53,9 @@ public static IServiceCollection AddHttpClients(this IServiceCollection services builder.AddNotificationsClient(); services.AddScoped(); + builder.AddOrgClient(); + services.AddScoped(); + return services; } } diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs index 98a8e8442..00178fa00 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs @@ -10,7 +10,7 @@ public class ResourceOwnerAdaptiveCardData public int NumberOfOpenRequests { get; set; } public int NumberOfRequestsStartingInMoreThanThreeMonths { get; set; } public int NumberOfRequestsStartingInLessThanThreeMonths { get; set; } - public int AverageTimeToHandleRequests { get; set; } + public double AverageTimeToHandleRequests { get; set; } public int AllocationChangesAwaitingTaskOwnerAction { get; set; } public int ProjectChangesAffectingNextThreeMonths { get; set; } internal IEnumerable PersonnelPositionsEndingWithNoFutureAllocation { get; set; } diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index c105361e0..9b95873c5 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using AdaptiveCards; using Azure.Messaging.ServiceBus; -using Fusion.Resources.Functions.ApiClients; using Fusion.Resources.Functions.ApiClients.ApiModels; using Fusion.Resources.Functions.Functions.Notifications.Models; using Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards; @@ -14,8 +13,9 @@ using Microsoft.Azure.WebJobs.ServiceBus; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using static Fusion.Resources.Functions.ApiClients.IResourcesApiClient; using static Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards.AdaptiveCardBuilder; +using Fusion.Resources.Functions.ApiClients; +using static Fusion.Resources.Functions.ApiClients.IResourcesApiClient; namespace Fusion.Resources.Functions.Functions.Notifications; @@ -24,14 +24,17 @@ public class ScheduledReportContentBuilderFunction private readonly ILogger _logger; private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; + private readonly IOrgClient _orgClient; public ScheduledReportContentBuilderFunction(ILogger logger, IResourcesApiClient resourcesApiClient, - INotificationApiClient notificationsClient) + INotificationApiClient notificationsClient, + IOrgClient orgClient) { _logger = logger; _resourceClient = resourcesApiClient; _notificationsClient = notificationsClient; + _orgClient = orgClient; } [FunctionName("scheduled-report-content-Builder-function")] @@ -98,7 +101,6 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD // Get all the personnel for the specific department var personnelForDepartment = await _resourceClient.GetAllPersonnelForDepartment(fullDepartment); personnelForDepartment = await GetPersonnelLeave(personnelForDepartment); - //1.Number of personnel: @@ -115,7 +117,6 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD // OK - Inkluderer nå leave.. var percentageOfTotalCapacity = FindTotalCapacityIncludingLeave(personnelForDepartment.ToList()); - // 3.New requests last week: // number of requests received last 7 days // Notat: En request kan ha blitt opprettet for 7 dager siden, men ikke oversendt til ressurseiere - Det kan være @@ -147,20 +148,21 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD .Count(x => !x.State.Contains(RequestState.completed.ToString()) && x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); - // TODO: + // TODO: MÅ TESTES OG VERIFISERES //7.Average time to handle request: //average number of days from request created/ sent to candidate is proposed - last 6 months + var averageTimeToHandleRequest = CalculateAverageTimeToHandleRequests(departmentRequests); - // TODO: + // TODO: GJENSTÅR //8.Allocation changes awaiting task owner action: //number of allocation changes made by resource owner awaiting task owner action //Må hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke på om det er gjort endringer her den siste tiden - // TODO: - //9.Project changes affecting next 3 months: + //9.Project changes affecting next 3 months: MÅ TESTES OG VERIFISERES //number of project changes(changes initiated by project / task) with a change affecting the next 3 months + var numberOfChangesAffectingNextThreeMonths = GetAllChangesForResourceDepartment(personnelForDepartment); - //10.Allocations ending soon with no future allocation: -Skal være ok ? + //10.Allocations ending soon with no future allocation: //list of allocations ending within next 3 months where the person allocated does not continue in the position(i.e.no future splits with the same person allocated) var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); @@ -183,8 +185,8 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD NumberOfOpenRequests = totalNumberOfOpenRequests, NumberOfRequestsStartingInMoreThanThreeMonths = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, NumberOfRequestsStartingInLessThanThreeMonths = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, - AverageTimeToHandleRequests = 0, - AllocationChangesAwaitingTaskOwnerAction = 0, + AverageTimeToHandleRequests = averageTimeToHandleRequest, + AllocationChangesAwaitingTaskOwnerAction = numberOfChangesAffectingNextThreeMonths, ProjectChangesAffectingNextThreeMonths = 0, PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent @@ -224,7 +226,7 @@ private IEnumerable FilterPersonnelWithoutFutureAllocations( private async Task> GetPersonnelLeave(IEnumerable listOfInternalPersonnel) { List newList = listOfInternalPersonnel.ToList(); - for (int i =0; i < newList.Count(); i++) + for (int i = 0; i < newList.Count(); i++) { var absence = await _resourceClient.GetLeaveForPersonnel(newList[i].AzureUniquePersonId.ToString()); newList[i].ApiPersonAbsences = absence.ToList(); @@ -238,6 +240,7 @@ private int FindTotalCapacityIncludingLeave(List listOf { //Calculated by total current workload for all personnel / (100 % workload x number of personnel - (total % leave)), //a.e.g. 10 people in department: 800 % current workload / (1000 % -120 % leave) = 91 % capacity in use + // We need to take into account the other types of allocations from absence-endpoint. var totalWorkLoad = 0.0; double? totalLeave = 0.0; @@ -245,6 +248,7 @@ private int FindTotalCapacityIncludingLeave(List listOf foreach (var personnel in listOfInternalPersonnel) { totalLeave += personnel.ApiPersonAbsences.Where(ab => ab.Type == ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); + totalWorkLoad += (double)personnel.ApiPersonAbsences.Where(ab => ab.Type != ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); totalWorkLoad += personnel.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); } @@ -253,6 +257,85 @@ private int FindTotalCapacityIncludingLeave(List listOf return Convert.ToInt32(totalPercentageInludeLeave); } + private int GetAllChangesForResourceDepartment(IEnumerable listOfInternalPersonnel) + { + // Find all active instances (we get projectId, positionId and instanceId from this) + // Then check if the changes are changes in split (duration, workload, location) - TODO: Check if there are other changes that should be accounted for + + var threeMonths = DateTime.UtcNow.AddMonths(3); + var today = DateTime.UtcNow; + + var listOfInternalPersonnelwithOnlyActiveProjects = listOfInternalPersonnel.SelectMany(per => per.PositionInstances.Where(pis => pis.IsActive || (pis.AppliesFrom < threeMonths && pis.AppliesFrom > today))); + + int totalChangesForDepartment = 0; + + foreach (var instance in listOfInternalPersonnelwithOnlyActiveProjects) + { + + var changeLogForPersonnel = _orgClient.GetChangeLog(instance.Project.Id.ToString(), instance.PositionId.ToString(), instance.InstanceId.ToString()); + var totalChanges = changeLogForPersonnel.Result.Events + .Where(ev => ev.ChangeType != ChangeType.PositionInstanceCreated.ToString() + && ev.ChangeType != ChangeType.PersonAssignedToPosition.ToString() + && ev.ChangeType != ChangeType.PositionInstanceAllocationStateChanged.ToString() + && ev.ChangeType != ChangeType.PositionInstanceParentPositionIdChanged.ToString() + && (ev.ChangeType.Equals(ChangeType.PositionInstanceAppliesToChanged) && ev.Instance.AppliesTo < threeMonths)); + + + totalChangesForDepartment += totalChanges.Count(); + } + return totalChangesForDepartment; + } + + + + + + private double CalculateAverageTimeToHandleRequests(IEnumerable listOfRequests) + { + // How to calculate: + // for each request: + // Find the workflow "created" and then find the date + // This should mean that task owner have created and sent the request to resource owner + // Find the workflow "proposal" and then find the date + // This should mean that the resource owner have done their bit + // TODO: Maybe we need to consider other states + // FIXME: We may need to filter out requests that are sent from resource-side (aka ResourceOwnerChange) + + + double days = 0; + int requestsHandledByResourceOwner = 0; + double totalNumberOfDays = 0.0; + + var threeMonthsAgo = DateTime.UtcNow.AddMonths(-3); + + var requestsLastThreemonths = listOfRequests + .Where(req => req.Created > threeMonthsAgo) + .Where(r => r.Workflow is not null) + .Where(r => r.Workflow.Steps is not null); + + foreach (var request in requestsLastThreemonths) + { + if (request.State.Equals(RequestState.created.ToString())) + continue; + + var dateForCreation = request.Workflow.Steps.FirstOrDefault(step => step.IsCompleted && step.Name.Equals("Created"))?.Completed.Value.DateTime; + var dateForApproval = request.Workflow.Steps.FirstOrDefault(step => step.IsCompleted && step.Name.Equals("Proposed"))?.Completed.Value.DateTime; + if (dateForCreation != null && dateForApproval != null) + { + requestsHandledByResourceOwner++; + var timespanDifference = dateForApproval - dateForCreation; + var differenceInDays = timespanDifference.Value.TotalDays; + totalNumberOfDays += differenceInDays; + } + } + + if (totalNumberOfDays > 0) + { + days = totalNumberOfDays / requestsHandledByResourceOwner; + } + return days; + } + private PersonnelContent CreatePersonnelContent(InternalPersonnelPerson person) { if (person == null) @@ -290,9 +373,11 @@ private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdapti { // Plasser denne en annen plass var baseUri = "https://fusion-s-portal-ci.azurewebsites.net/apps/personnel-allocation/"; - var avdelingsId = "52586050"; + var avdelingsId = "52586050"; // Må finne ut av hvor man får denne ID'en til avdelingen. Mulig man må hente det fra context? + var formatDoubleToHaveOneDecimal = "F1"; + var card = new AdaptiveCardBuilder() .AddHeading($"**Weekly summary - {departmentIdentifier}**") .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfPersonnel.ToString(), "Number of personnel")) @@ -301,17 +386,12 @@ private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdapti .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfOpenRequests.ToString(), "Open requests")) .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInLessThanThreeMonths.ToString(), "Requests with start date < 3 months")) .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInMoreThanThreeMonths.ToString(), "Requests with start date > 3 months")) - - // Not finished - .AddColumnSet(new AdaptiveCardColumn(/*(cardData.AverageTimeToHandleRequests.ToString()*/ "NA", "Average time to handle request")) // WIP + .AddColumnSet(new AdaptiveCardColumn(cardData.AverageTimeToHandleRequests.ToString(formatDoubleToHaveOneDecimal), "Average time to handle request", "days")) .AddColumnSet(new AdaptiveCardColumn(/*cardData.AllocationChangesAwaitingTaskOwnerAction.ToString()*/ "NA", "Allocation changes awaiting task owner action")) // WIP - .AddColumnSet(new AdaptiveCardColumn(/*cardData.ProjectChangesAffectingNextThreeMonths.ToString()*/ "NA", "Project changes affecting next 3 months")) // WIP - - .AddListContainer("Positions ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") + .AddColumnSet(new AdaptiveCardColumn(cardData.ProjectChangesAffectingNextThreeMonths.ToString(), "Project changes affecting next 3 months")) + .AddListContainer("Allocations ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") - .AddActionButton("Go to Personnel allocation app", $"{baseUri}{avdelingsId}") - .Build(); return card; From be6dd2c04206bd274d7c3e9bf388ee2fae427fe0 Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Mon, 11 Dec 2023 10:59:39 +0100 Subject: [PATCH 05/13] Added AllocationChangesAwaitingTaskOwnerAction --- .../ScheduledReportContentBuilderFunction.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 9b95873c5..4dc2a2e98 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -136,17 +136,21 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD //number of requests with start date within less than 3 months // Filter to only inlclude the ones that have start-date in less than 3 months and start-date after today and is not complete and has no proposedPerson assigned to them var numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination = departmentRequests - .Count(x => !x.State.Contains(RequestState.completed.ToString()) && - (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && - x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); + .Count(x => x.OrgPositionInstance != null && + x.State != null && + !x.State.Contains(RequestState.completed.ToString()) && + (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && + x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); //6.Requests with start-date > 3 months: //number of requests with start date later than next 3 months // Filter to only include the ones that have start-date in more than 3 months AND state not completed var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests - .Count(x => !x.State.Contains(RequestState.completed.ToString()) && - x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); + .Count(x => x.OrgPositionInstance != null && + x.State != null && + !x.State.Contains(RequestState.completed.ToString()) && + x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); // TODO: MÅ TESTES OG VERIFISERES //7.Average time to handle request: @@ -157,6 +161,7 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD //8.Allocation changes awaiting task owner action: //number of allocation changes made by resource owner awaiting task owner action //Må hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke på om det er gjort endringer her den siste tiden + var numberOfAllocationchangesAwaitingTaskOwnerAction = GetchangesAwaitingTaskOwnerAction(departmentRequests); //9.Project changes affecting next 3 months: MÅ TESTES OG VERIFISERES //number of project changes(changes initiated by project / task) with a change affecting the next 3 months @@ -186,8 +191,8 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD NumberOfRequestsStartingInMoreThanThreeMonths = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, NumberOfRequestsStartingInLessThanThreeMonths = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, AverageTimeToHandleRequests = averageTimeToHandleRequest, - AllocationChangesAwaitingTaskOwnerAction = numberOfChangesAffectingNextThreeMonths, - ProjectChangesAffectingNextThreeMonths = 0, + AllocationChangesAwaitingTaskOwnerAction = numberOfAllocationchangesAwaitingTaskOwnerAction, + ProjectChangesAffectingNextThreeMonths = numberOfChangesAffectingNextThreeMonths, PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent }, @@ -286,8 +291,8 @@ private int GetAllChangesForResourceDepartment(IEnumerable listOfRequests) + => listOfRequests.Where((req => req.Type.Equals("ResourceOwnerChange"))).Where(req => req.Workflow.Steps.Any(step => !step.IsCompleted && step.Name.Equals("Accept"))).ToList().Count(); private double CalculateAverageTimeToHandleRequests(IEnumerable listOfRequests) @@ -387,7 +392,7 @@ private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdapti .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInLessThanThreeMonths.ToString(), "Requests with start date < 3 months")) .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInMoreThanThreeMonths.ToString(), "Requests with start date > 3 months")) .AddColumnSet(new AdaptiveCardColumn(cardData.AverageTimeToHandleRequests.ToString(formatDoubleToHaveOneDecimal), "Average time to handle request", "days")) - .AddColumnSet(new AdaptiveCardColumn(/*cardData.AllocationChangesAwaitingTaskOwnerAction.ToString()*/ "NA", "Allocation changes awaiting task owner action")) // WIP + .AddColumnSet(new AdaptiveCardColumn(cardData.AllocationChangesAwaitingTaskOwnerAction.ToString(), "Allocation changes awaiting task owner action")) // WIP .AddColumnSet(new AdaptiveCardColumn(cardData.ProjectChangesAffectingNextThreeMonths.ToString(), "Project changes affecting next 3 months")) .AddListContainer("Allocations ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") From ba37e11381cef7efdfaa98a38501a8f5a5275baf Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Tue, 12 Dec 2023 07:53:36 +0100 Subject: [PATCH 06/13] Cleanup and fixes to notifications --- .../ApiClients/IOrgApiClient.cs | 18 +-- .../ScheduledReportContentBuilderFunction.cs | 118 +++++++----------- .../ScheduledReportTimerTriggerFunction.cs | 3 +- 3 files changed, 55 insertions(+), 84 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs index 866448585..2b39ef392 100644 --- a/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs @@ -58,16 +58,16 @@ public class ApiInstanceSnapshot } } -public enum ChangeType +public class ChangeType { - PositionInstanceCreated, - PersonAssignedToPosition, - PositionInstanceAllocationStateChanged, - PositionInstanceAppliesToChanged, - PositionInstanceAppliesFromChanged, - PositionInstanceParentPositionIdChanged, - PositionInstancePercentChanged, - PositionInstanceLocationChanged + public static string PositionInstanceCreated = "PositionInstanceCreated"; + public static string PersonAssignedToPosition = "PersonAssignedToPosition"; + public static string PositionInstanceAllocationStateChanged = "PositionInstanceAllocationStateChanged"; + public static string PositionInstanceAppliesToChanged = "PositionInstanceAppliesToChanged"; + public static string PositionInstanceAppliesFromChanged = "PositionInstanceAppliesFromChanged"; + public static string PositionInstanceParentPositionIdChanged = "PositionInstanceParentPositionIdChanged"; + public static string PositionInstancePercentChanged = "PositionInstancePercentChanged"; + public static string PositionInstanceLocationChanged = "PositionInstanceLocationChanged"; } #endregion \ No newline at end of file diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 4dc2a2e98..7025083e9 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -25,6 +25,7 @@ public class ScheduledReportContentBuilderFunction private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; private readonly IOrgClient _orgClient; + private static readonly string formatDoubleToHaveOneDecimal = "F1"; public ScheduledReportContentBuilderFunction(ILogger logger, IResourcesApiClient resourcesApiClient, @@ -58,7 +59,7 @@ public async Task RunAsync( switch (dto.Role) { case NotificationRoleType.ResourceOwner: - await BuildContentForResourceOwner(azureUniqueId, dto.FullDepartment); + await BuildContentForResourceOwner(azureUniqueId, dto.FullDepartment, dto.DepartmentSapId); break; case NotificationRoleType.TaskOwner: await BuildContentForTaskOwner(azureUniqueId); @@ -89,91 +90,65 @@ private async Task BuildContentForTaskOwner(Guid azureUniqueId) throw new NotImplementedException(); } - private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullDepartment) + private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullDepartment, string departmentSapId) { var threeMonthsFuture = DateTime.UtcNow.AddMonths(3); var today = DateTime.UtcNow; var sevenDaysAgo = DateTime.UtcNow.AddDays(-7); - // Get all requests for specific Department regardsless of state + // Get all requests for department regardsless of state var departmentRequests = await _resourceClient.GetAllRequestsForDepartment(fullDepartment); - - // Get all the personnel for the specific department + // Get all the personnel for the department var personnelForDepartment = await _resourceClient.GetAllPersonnelForDepartment(fullDepartment); + // Adding leave personnelForDepartment = await GetPersonnelLeave(personnelForDepartment); - //1.Number of personnel: - //number of personnel in the department(one specific org unit) - // Hvordan finne? - // Hent ut alle ressursene for en gitt avdeling - // OK? + //1.Number of personnel var numberOfPersonnel = personnelForDepartment.Count(); //2.Capacity in use: - //capacity in use by %. - //Calculated by total current workload for all personnel / (100 % workload x number of personnel - (total % leave)), - //a.e.g. 10 people in department: 800 % current workload / (1000 % -120 % leave) = 91 % capacity in use - // OK - Inkluderer nå leave.. var percentageOfTotalCapacity = FindTotalCapacityIncludingLeave(personnelForDepartment.ToList()); - // 3.New requests last week: - // number of requests received last 7 days - // Notat: En request kan ha blitt opprettet for 7 dager siden, men ikke oversendt til ressurseiere - Det kan være - // Inkluderer foreløpig alle requestene uavhengig av hvilken state de er - var numberOfRequestsLastWeek = departmentRequests.Where(req => req.Created > sevenDaysAgo && !req.IsDraft).Count(); - - + // 3.New requests last week (7 days) + var numberOfRequestsLastWeek = departmentRequests.Count(req => !req.Type.Equals("ResourceOwnerChange") && req.Created > sevenDaysAgo && !req.IsDraft); - //4.Open request: - //number of requests with no proposed candidate - // Only to include those requests which have state approval (this means that the resource owner needs to process the requests in some way) - // OK? - Må sjekkes og finne noen som har approval... - var totalNumberOfOpenRequests = departmentRequests.Count(req => !req.HasProposedPerson && !req.State.Contains(RequestState.completed.ToString())); + //4.Open request (no proposedPerson) + var totalNumberOfOpenRequests = departmentRequests.Count(req => !req.Type.Equals("ResourceOwnerChange") && !req.HasProposedPerson && !req.State.Equals("completed", StringComparison.OrdinalIgnoreCase)); - //5.Requests with start-date < 3 months: - //number of requests with start date within less than 3 months - // Filter to only inlclude the ones that have start-date in less than 3 months and start-date after today and is not complete and has no proposedPerson assigned to them + //5.Requests with start-date < 3 months var numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination = departmentRequests .Count(x => x.OrgPositionInstance != null && x.State != null && - !x.State.Contains(RequestState.completed.ToString()) && + !x.State.Equals("completed") && (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); //6.Requests with start-date > 3 months: - //number of requests with start date later than next 3 months - // Filter to only include the ones that have start-date in more than 3 months AND state not completed var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests .Count(x => x.OrgPositionInstance != null && x.State != null && - !x.State.Contains(RequestState.completed.ToString()) && + !x.State.Contains("completed") && x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); - // TODO: MÅ TESTES OG VERIFISERES - //7.Average time to handle request: - //average number of days from request created/ sent to candidate is proposed - last 6 months + //7.Average time to handle request (last 3 months): var averageTimeToHandleRequest = CalculateAverageTimeToHandleRequests(departmentRequests); - // TODO: GJENSTÅR //8.Allocation changes awaiting task owner action: //number of allocation changes made by resource owner awaiting task owner action //Må hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke på om det er gjort endringer her den siste tiden var numberOfAllocationchangesAwaitingTaskOwnerAction = GetchangesAwaitingTaskOwnerAction(departmentRequests); - //9.Project changes affecting next 3 months: MÅ TESTES OG VERIFISERES + //9.Project changes affecting next 3 months //number of project changes(changes initiated by project / task) with a change affecting the next 3 months var numberOfChangesAffectingNextThreeMonths = GetAllChangesForResourceDepartment(personnelForDepartment); - //10.Allocations ending soon with no future allocation: - //list of allocations ending within next 3 months where the person allocated does not continue in the position(i.e.no future splits with the same person allocated) + //10.Allocations ending soon with no future allocation var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); - //11.Personnel with more than 100 % workload: -OK - //(as in current pilot, but remove "FTE") list of persons with total allocation > 100 %, total % workload should be visible after person name - // TODO: Fiks formatering og oppdeling av innhold her + //11.Personnel with more than 100 % workload var listOfPersonnelsWithMoreThan100Percent = personnelForDepartment.Where(p => p.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum() > 100); var listOfPersonnelForDepartmentWithMoreThan100Percent = @@ -196,7 +171,7 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent }, - fullDepartment); + fullDepartment, departmentSapId); var sendNotification = await _notificationsClient.SendNotification( new SendNotificationsRequest() @@ -279,10 +254,10 @@ private int GetAllChangesForResourceDepartment(IEnumerable ev.ChangeType != ChangeType.PositionInstanceCreated.ToString() - && ev.ChangeType != ChangeType.PersonAssignedToPosition.ToString() - && ev.ChangeType != ChangeType.PositionInstanceAllocationStateChanged.ToString() - && ev.ChangeType != ChangeType.PositionInstanceParentPositionIdChanged.ToString() + .Where(ev => ev.ChangeType != ChangeType.PositionInstanceCreated + && ev.ChangeType != ChangeType.PersonAssignedToPosition + && ev.ChangeType != ChangeType.PositionInstanceAllocationStateChanged + && ev.ChangeType != ChangeType.PositionInstanceParentPositionIdChanged && (ev.ChangeType.Equals(ChangeType.PositionInstanceAppliesToChanged) && ev.Instance.AppliesTo < threeMonths)); @@ -292,39 +267,38 @@ private int GetAllChangesForResourceDepartment(IEnumerable listOfRequests) - => listOfRequests.Where((req => req.Type.Equals("ResourceOwnerChange"))).Where(req => req.Workflow.Steps.Any(step => !step.IsCompleted && step.Name.Equals("Accept"))).ToList().Count(); + => listOfRequests.Where((req => req.Type.Equals("ResourceOwnerChange"))).Where(req => req.Workflow.Steps.Any(step => step.Name.Equals("Accept") && !step.IsCompleted)).ToList().Count(); private double CalculateAverageTimeToHandleRequests(IEnumerable listOfRequests) { - // How to calculate: - // for each request: - // Find the workflow "created" and then find the date - // This should mean that task owner have created and sent the request to resource owner - // Find the workflow "proposal" and then find the date - // This should mean that the resource owner have done their bit - // TODO: Maybe we need to consider other states - // FIXME: We may need to filter out requests that are sent from resource-side (aka ResourceOwnerChange) - - - double days = 0; + /* How to calculate: + * + * Find the workflow "created" and then find the date + * This should mean that task owner have created and sent the request to resource owner + * Find the workflow "proposal" and then find the date + * This should mean that the resource owner have done their bit + * TODO: Maybe we need to consider other states + */ + + double days = 0.0; int requestsHandledByResourceOwner = 0; double totalNumberOfDays = 0.0; var threeMonthsAgo = DateTime.UtcNow.AddMonths(-3); - var requestsLastThreemonths = listOfRequests + var requestsLastThreeMonthsWithoutResourceOwnerChangeRequest = listOfRequests .Where(req => req.Created > threeMonthsAgo) .Where(r => r.Workflow is not null) - .Where(r => r.Workflow.Steps is not null); + .Where(r => r.Workflow.Steps is not null).Where((req => !req.Type.Equals("ResourceOwnerChange"))); - foreach (var request in requestsLastThreemonths) + foreach (var request in requestsLastThreeMonthsWithoutResourceOwnerChangeRequest) { - if (request.State.Equals(RequestState.created.ToString())) + if (request.State.Equals("created")) continue; - var dateForCreation = request.Workflow.Steps.FirstOrDefault(step => step.IsCompleted && step.Name.Equals("Created"))?.Completed.Value.DateTime; - var dateForApproval = request.Workflow.Steps.FirstOrDefault(step => step.IsCompleted && step.Name.Equals("Proposed"))?.Completed.Value.DateTime; + var dateForCreation = request.Workflow.Steps.FirstOrDefault(step => step.Name.Equals("Created") && step.IsCompleted)?.Completed.Value.DateTime; + var dateForApproval = request.Workflow.Steps.FirstOrDefault(step => step.Name.Equals("Proposed") && step.IsCompleted)?.Completed.Value.DateTime; if (dateForCreation != null && dateForApproval != null) { requestsHandledByResourceOwner++; @@ -374,14 +348,10 @@ private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson p } private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, - string departmentIdentifier) + string departmentIdentifier, string departmentSapId) { - // Plasser denne en annen plass + // FIXME:Plasser denne en annen plass var baseUri = "https://fusion-s-portal-ci.azurewebsites.net/apps/personnel-allocation/"; - var avdelingsId = "52586050"; // Må finne ut av hvor man får denne ID'en til avdelingen. Mulig man må hente det fra context? - - - var formatDoubleToHaveOneDecimal = "F1"; var card = new AdaptiveCardBuilder() .AddHeading($"**Weekly summary - {departmentIdentifier}**") @@ -392,11 +362,11 @@ private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdapti .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInLessThanThreeMonths.ToString(), "Requests with start date < 3 months")) .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInMoreThanThreeMonths.ToString(), "Requests with start date > 3 months")) .AddColumnSet(new AdaptiveCardColumn(cardData.AverageTimeToHandleRequests.ToString(formatDoubleToHaveOneDecimal), "Average time to handle request", "days")) - .AddColumnSet(new AdaptiveCardColumn(cardData.AllocationChangesAwaitingTaskOwnerAction.ToString(), "Allocation changes awaiting task owner action")) // WIP + .AddColumnSet(new AdaptiveCardColumn(cardData.AllocationChangesAwaitingTaskOwnerAction.ToString(), "Allocation changes awaiting task owner action")) .AddColumnSet(new AdaptiveCardColumn(cardData.ProjectChangesAffectingNextThreeMonths.ToString(), "Project changes affecting next 3 months")) .AddListContainer("Allocations ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") - .AddActionButton("Go to Personnel allocation app", $"{baseUri}{avdelingsId}") + .AddActionButton("Go to Personnel allocation app", $"{baseUri}/{departmentSapId}") .Build(); return card; diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportTimerTriggerFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportTimerTriggerFunction.cs index 2b437ec1b..aceda758c 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportTimerTriggerFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportTimerTriggerFunction.cs @@ -84,7 +84,8 @@ private async Task SendResourceOwnersToQueue(ServiceBusSender sender) { AzureUniqueId = resourceOwner.AzureUniqueId, FullDepartment = resourceOwner.FullDepartment, - Role = NotificationRoleType.ResourceOwner + Role = NotificationRoleType.ResourceOwner, + DepartmentSapId = resourceOwner.DepartmentSapId }); } catch (Exception e) From de45b7abeedcbbd3b339d19796b62d84625a403e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A5land?= Date: Tue, 12 Dec 2023 09:48:09 +0100 Subject: [PATCH 07/13] null referance guards added --- .../ScheduledReportContentBuilderFunction.cs | 153 +++++++++++------- 1 file changed, 91 insertions(+), 62 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 7025083e9..fba291eaa 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -25,7 +25,7 @@ public class ScheduledReportContentBuilderFunction private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; private readonly IOrgClient _orgClient; - private static readonly string formatDoubleToHaveOneDecimal = "F1"; + private const string FormatDoubleToHaveOneDecimal = "F1"; public ScheduledReportContentBuilderFunction(ILogger logger, IResourcesApiClient resourcesApiClient, @@ -111,34 +111,38 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD var percentageOfTotalCapacity = FindTotalCapacityIncludingLeave(personnelForDepartment.ToList()); // 3.New requests last week (7 days) - var numberOfRequestsLastWeek = departmentRequests.Count(req => !req.Type.Equals("ResourceOwnerChange") && req.Created > sevenDaysAgo && !req.IsDraft); + var numberOfRequestsLastWeek = departmentRequests.Count(req => + req.Type != null && !req.Type.Equals("ResourceOwnerChange") && req.Created > sevenDaysAgo && !req.IsDraft); //4.Open request (no proposedPerson) - var totalNumberOfOpenRequests = departmentRequests.Count(req => !req.Type.Equals("ResourceOwnerChange") && !req.HasProposedPerson && !req.State.Equals("completed", StringComparison.OrdinalIgnoreCase)); + var totalNumberOfOpenRequests = departmentRequests.Count(req => + req.State != null && req.Type != null && !req.Type.Equals("ResourceOwnerChange") && + !req.HasProposedPerson && + !req.State.Equals("completed", StringComparison.OrdinalIgnoreCase)); //5.Requests with start-date < 3 months var numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination = departmentRequests - .Count(x => x.OrgPositionInstance != null && - x.State != null && - !x.State.Equals("completed") && - (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && - x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); + .Count(x => x.OrgPositionInstance != null && + x.State != null && + !x.State.Equals("completed") && + (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && + x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); //6.Requests with start-date > 3 months: var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests - .Count(x => x.OrgPositionInstance != null && - x.State != null && - !x.State.Contains("completed") && - x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); + .Count(x => x.OrgPositionInstance != null && + x.State != null && + !x.State.Contains("completed") && + x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); //7.Average time to handle request (last 3 months): var averageTimeToHandleRequest = CalculateAverageTimeToHandleRequests(departmentRequests); //8.Allocation changes awaiting task owner action: //number of allocation changes made by resource owner awaiting task owner action - //Må hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke på om det er gjort endringer her den siste tiden + //M� hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke p� om det er gjort endringer her den siste tiden var numberOfAllocationchangesAwaitingTaskOwnerAction = GetchangesAwaitingTaskOwnerAction(departmentRequests); //9.Project changes affecting next 3 months @@ -155,22 +159,22 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD listOfPersonnelsWithMoreThan100Percent.Select(p => CreatePersonnelWithTBEContent(p)); - - var card = ResourceOwnerAdaptiveCardBuilder(new ResourceOwnerAdaptiveCardData - { - TotalNumberOfPersonnel = numberOfPersonnel, - TotalCapacityInUsePercentage = percentageOfTotalCapacity, - NumberOfRequestsLastWeek = numberOfRequestsLastWeek, - NumberOfOpenRequests = totalNumberOfOpenRequests, - NumberOfRequestsStartingInMoreThanThreeMonths = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, - NumberOfRequestsStartingInLessThanThreeMonths = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, - AverageTimeToHandleRequests = averageTimeToHandleRequest, - AllocationChangesAwaitingTaskOwnerAction = numberOfAllocationchangesAwaitingTaskOwnerAction, - ProjectChangesAffectingNextThreeMonths = numberOfChangesAffectingNextThreeMonths, - PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, - PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent - }, + { + TotalNumberOfPersonnel = numberOfPersonnel, + TotalCapacityInUsePercentage = percentageOfTotalCapacity, + NumberOfRequestsLastWeek = numberOfRequestsLastWeek, + NumberOfOpenRequests = totalNumberOfOpenRequests, + NumberOfRequestsStartingInMoreThanThreeMonths = + numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, + NumberOfRequestsStartingInLessThanThreeMonths = + numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, + AverageTimeToHandleRequests = averageTimeToHandleRequest, + AllocationChangesAwaitingTaskOwnerAction = numberOfAllocationchangesAwaitingTaskOwnerAction, + ProjectChangesAffectingNextThreeMonths = numberOfChangesAffectingNextThreeMonths, + PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, + PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent + }, fullDepartment, departmentSapId); var sendNotification = await _notificationsClient.SendNotification( @@ -203,7 +207,8 @@ private IEnumerable FilterPersonnelWithoutFutureAllocations( return personnelWithoutFutureAllocations.Select(p => CreatePersonnelContent(p)); } - private async Task> GetPersonnelLeave(IEnumerable listOfInternalPersonnel) + private async Task> GetPersonnelLeave( + IEnumerable listOfInternalPersonnel) { List newList = listOfInternalPersonnel.ToList(); for (int i = 0; i < newList.Count(); i++) @@ -227,8 +232,10 @@ private int FindTotalCapacityIncludingLeave(List listOf foreach (var personnel in listOfInternalPersonnel) { - totalLeave += personnel.ApiPersonAbsences.Where(ab => ab.Type == ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); - totalWorkLoad += (double)personnel.ApiPersonAbsences.Where(ab => ab.Type != ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); + totalLeave += personnel.ApiPersonAbsences.Where(ab => ab.Type == ApiAbsenceType.Absence && ab.IsActive) + .Select(ab => ab.AbsencePercentage).Sum(); + totalWorkLoad += (double)personnel.ApiPersonAbsences + .Where(ab => ab.Type != ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); totalWorkLoad += personnel.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); } @@ -245,41 +252,51 @@ private int GetAllChangesForResourceDepartment(IEnumerable per.PositionInstances.Where(pis => pis.IsActive || (pis.AppliesFrom < threeMonths && pis.AppliesFrom > today))); + var listOfInternalPersonnelwithOnlyActiveProjects = listOfInternalPersonnel.SelectMany(per => + per.PositionInstances.Where(pis => + pis.IsActive || (pis.AppliesFrom < threeMonths && pis.AppliesFrom > today))); int totalChangesForDepartment = 0; foreach (var instance in listOfInternalPersonnelwithOnlyActiveProjects) { + if (instance.Project == null) + continue; - var changeLogForPersonnel = _orgClient.GetChangeLog(instance.Project.Id.ToString(), instance.PositionId.ToString(), instance.InstanceId.ToString()); + var changeLogForPersonnel = _orgClient.GetChangeLog(instance.Project.Id.ToString(), + instance.PositionId.ToString(), instance.InstanceId.ToString()); var totalChanges = changeLogForPersonnel.Result.Events - .Where(ev => ev.ChangeType != ChangeType.PositionInstanceCreated - && ev.ChangeType != ChangeType.PersonAssignedToPosition - && ev.ChangeType != ChangeType.PositionInstanceAllocationStateChanged - && ev.ChangeType != ChangeType.PositionInstanceParentPositionIdChanged - && (ev.ChangeType.Equals(ChangeType.PositionInstanceAppliesToChanged) && ev.Instance.AppliesTo < threeMonths)); + .Where(ev => ev.Instance != null + && ev.ChangeType != ChangeType.PositionInstanceCreated + && ev.ChangeType != ChangeType.PersonAssignedToPosition + && ev.ChangeType != ChangeType.PositionInstanceAllocationStateChanged + && ev.ChangeType != ChangeType.PositionInstanceParentPositionIdChanged + && (ev.ChangeType.Equals(ChangeType.PositionInstanceAppliesToChanged) && + ev.Instance.AppliesTo < threeMonths)); totalChangesForDepartment += totalChanges.Count(); } + return totalChangesForDepartment; } - public int GetchangesAwaitingTaskOwnerAction(IEnumerable listOfRequests) - => listOfRequests.Where((req => req.Type.Equals("ResourceOwnerChange"))).Where(req => req.Workflow.Steps.Any(step => step.Name.Equals("Accept") && !step.IsCompleted)).ToList().Count(); + private int GetchangesAwaitingTaskOwnerAction(IEnumerable listOfRequests) + => listOfRequests.Where((req => req.Type is "ResourceOwnerChange")).Where(req => + req.Workflow != null && req.Workflow.Steps.Any(step => step.Name.Equals("Accept") && !step.IsCompleted)) + .ToList().Count(); private double CalculateAverageTimeToHandleRequests(IEnumerable listOfRequests) { /* How to calculate: - * + * * Find the workflow "created" and then find the date * This should mean that task owner have created and sent the request to resource owner * Find the workflow "proposal" and then find the date * This should mean that the resource owner have done their bit - * TODO: Maybe we need to consider other states - */ + * TODO: Maybe we need to consider other states + */ double days = 0.0; int requestsHandledByResourceOwner = 0; @@ -290,15 +307,17 @@ private double CalculateAverageTimeToHandleRequests(IEnumerable req.Created > threeMonthsAgo) .Where(r => r.Workflow is not null) - .Where(r => r.Workflow.Steps is not null).Where((req => !req.Type.Equals("ResourceOwnerChange"))); + .Where(r => true).Where((req => req.Type != null && !req.Type.Equals("ResourceOwnerChange"))); foreach (var request in requestsLastThreeMonthsWithoutResourceOwnerChangeRequest) { - if (request.State.Equals("created")) + if (request.State is "created") continue; - var dateForCreation = request.Workflow.Steps.FirstOrDefault(step => step.Name.Equals("Created") && step.IsCompleted)?.Completed.Value.DateTime; - var dateForApproval = request.Workflow.Steps.FirstOrDefault(step => step.Name.Equals("Proposed") && step.IsCompleted)?.Completed.Value.DateTime; + var dateForCreation = request.Workflow.Steps + .FirstOrDefault(step => step.Name.Equals("Created") && step.IsCompleted)?.Completed.Value.DateTime; + var dateForApproval = request.Workflow.Steps + .FirstOrDefault(step => step.Name.Equals("Proposed") && step.IsCompleted)?.Completed.Value.DateTime; if (dateForCreation != null && dateForApproval != null) { requestsHandledByResourceOwner++; @@ -312,6 +331,7 @@ private double CalculateAverageTimeToHandleRequests(IEnumerable 3 months")) - .AddColumnSet(new AdaptiveCardColumn(cardData.AverageTimeToHandleRequests.ToString(formatDoubleToHaveOneDecimal), "Average time to handle request", "days")) - .AddColumnSet(new AdaptiveCardColumn(cardData.AllocationChangesAwaitingTaskOwnerAction.ToString(), "Allocation changes awaiting task owner action")) - .AddColumnSet(new AdaptiveCardColumn(cardData.ProjectChangesAffectingNextThreeMonths.ToString(), "Project changes affecting next 3 months")) - .AddListContainer("Allocations ending soon with no future allocation:", cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") - .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") - .AddActionButton("Go to Personnel allocation app", $"{baseUri}/{departmentSapId}") - .Build(); + .AddHeading($"**Weekly summary - {departmentIdentifier}**") + .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfPersonnel.ToString(), "Number of personnel")) + .AddColumnSet(new AdaptiveCardColumn(cardData.TotalCapacityInUsePercentage.ToString(), "Capacity in use", + "%")) + .AddColumnSet( + new AdaptiveCardColumn(cardData.NumberOfRequestsLastWeek.ToString(), "New requests last week")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfOpenRequests.ToString(), "Open requests")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInLessThanThreeMonths.ToString(), + "Requests with start date < 3 months")) + .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInMoreThanThreeMonths.ToString(), + "Requests with start date > 3 months")) + .AddColumnSet(new AdaptiveCardColumn( + cardData.AverageTimeToHandleRequests.ToString(FormatDoubleToHaveOneDecimal), + "Average time to handle request", "days")) + .AddColumnSet(new AdaptiveCardColumn(cardData.AllocationChangesAwaitingTaskOwnerAction.ToString(), + "Allocation changes awaiting task owner action")) + .AddColumnSet(new AdaptiveCardColumn(cardData.ProjectChangesAffectingNextThreeMonths.ToString(), + "Project changes affecting next 3 months")) + .AddListContainer("Allocations ending soon with no future allocation:", + cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") + .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, + "FullName", "TotalWorkload") + .AddActionButton("Go to Personnel allocation app", $"{baseUri}/{departmentSapId}") + .Build(); return card; } - } \ No newline at end of file From bdb17bd9b27d345889e67e773d5ece7212efc26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A5land?= Date: Wed, 13 Dec 2023 10:55:27 +0100 Subject: [PATCH 08/13] fusion portal url added --- .../ScheduledReportContentBuilderFunction.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index fba291eaa..114fdf018 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -16,6 +16,9 @@ using static Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards.AdaptiveCardBuilder; using Fusion.Resources.Functions.ApiClients; using static Fusion.Resources.Functions.ApiClients.IResourcesApiClient; +using Fusion.Integration; +using Fusion.Integration.Configuration; +using Fusion.Integration.ServiceDiscovery; namespace Fusion.Resources.Functions.Functions.Notifications; @@ -25,17 +28,19 @@ public class ScheduledReportContentBuilderFunction private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; private readonly IOrgClient _orgClient; + private readonly IFusionEndpointResolver _endpointResolver; private const string FormatDoubleToHaveOneDecimal = "F1"; public ScheduledReportContentBuilderFunction(ILogger logger, IResourcesApiClient resourcesApiClient, INotificationApiClient notificationsClient, - IOrgClient orgClient) + IOrgClient orgClient, IFusionEndpointResolver endpointResolver) { _logger = logger; _resourceClient = resourcesApiClient; _notificationsClient = notificationsClient; _orgClient = orgClient; + _endpointResolver = endpointResolver; } [FunctionName("scheduled-report-content-Builder-function")] @@ -367,12 +372,11 @@ private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson p return personnelContent; } - private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, + private async Task ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, string departmentIdentifier, string departmentSapId) { - // FIXME:Plasser denne en annen plass - var baseUri = "https://fusion-s-portal-ci.azurewebsites.net/apps/personnel-allocation/"; - + var portalUri = await _endpointResolver.ResolveEndpointAsync(FusionEndpoint.Portal); + var personnelAllocationUri = $"{portalUri}/apps/personnel-allocation/{departmentSapId}"; var card = new AdaptiveCardBuilder() .AddHeading($"**Weekly summary - {departmentIdentifier}**") .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfPersonnel.ToString(), "Number of personnel")) @@ -396,7 +400,7 @@ private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdapti cardData.PersonnelPositionsEndingWithNoFutureAllocation, "FullName", "EndingPosition") .AddListContainer("Personnel with more than 100% workload:", cardData.PersonnelAllocatedMoreThan100Percent, "FullName", "TotalWorkload") - .AddActionButton("Go to Personnel allocation app", $"{baseUri}/{departmentSapId}") + .AddActionButton("Go to Personnel allocation app", personnelAllocationUri) .Build(); return card; From 9fabb0714b07a58e5bb0580d41b54c391159187b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A5land?= Date: Thu, 14 Dec 2023 20:48:05 +0100 Subject: [PATCH 09/13] portal url colleced from settings --- .../ScheduledReportContentBuilderFunction.cs | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 114fdf018..48c0cb8af 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -19,6 +19,7 @@ using Fusion.Integration; using Fusion.Integration.Configuration; using Fusion.Integration.ServiceDiscovery; +using Microsoft.Extensions.Configuration; namespace Fusion.Resources.Functions.Functions.Notifications; @@ -28,19 +29,19 @@ public class ScheduledReportContentBuilderFunction private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; private readonly IOrgClient _orgClient; - private readonly IFusionEndpointResolver _endpointResolver; private const string FormatDoubleToHaveOneDecimal = "F1"; + private readonly IConfiguration _configuration; public ScheduledReportContentBuilderFunction(ILogger logger, IResourcesApiClient resourcesApiClient, INotificationApiClient notificationsClient, - IOrgClient orgClient, IFusionEndpointResolver endpointResolver) + IOrgClient orgClient, IConfiguration configuration) { _logger = logger; _resourceClient = resourcesApiClient; _notificationsClient = notificationsClient; _orgClient = orgClient; - _endpointResolver = endpointResolver; + _configuration = configuration; } [FunctionName("scheduled-report-content-Builder-function")] @@ -303,16 +304,16 @@ private double CalculateAverageTimeToHandleRequests(IEnumerable req.Created > threeMonthsAgo) .Where(r => r.Workflow is not null) - .Where(r => true).Where((req => req.Type != null && !req.Type.Equals("ResourceOwnerChange"))); + .Where(_ => true) + .Where((req => req.Type != null && !req.Type.Equals("ResourceOwnerChange"))); foreach (var request in requestsLastThreeMonthsWithoutResourceOwnerChangeRequest) { @@ -332,12 +333,10 @@ private double CalculateAverageTimeToHandleRequests(IEnumerable 0) - { - days = totalNumberOfDays / requestsHandledByResourceOwner; - } + if (!(totalNumberOfDays > 0)) + return 0; - return days; + return totalNumberOfDays / requestsHandledByResourceOwner; } private PersonnelContent CreatePersonnelContent(InternalPersonnelPerson person) @@ -375,8 +374,7 @@ private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson p private async Task ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, string departmentIdentifier, string departmentSapId) { - var portalUri = await _endpointResolver.ResolveEndpointAsync(FusionEndpoint.Portal); - var personnelAllocationUri = $"{portalUri}/apps/personnel-allocation/{departmentSapId}"; + var personnelAllocationUri = $"{PortalUri()}apps/personnel-allocation/{departmentSapId}"; var card = new AdaptiveCardBuilder() .AddHeading($"**Weekly summary - {departmentIdentifier}**") .AddColumnSet(new AdaptiveCardColumn(cardData.TotalNumberOfPersonnel.ToString(), "Number of personnel")) @@ -405,4 +403,12 @@ private async Task ResourceOwnerAdaptiveCardBuilder(ResourceOwnerA return card; } + + private string PortalUri() + { + var portalUri = _configuration["Endpoints_portal"] ?? "https://fusion.equinor.com/"; + if (!portalUri.EndsWith("/")) + portalUri += "/"; + return portalUri; + } } \ No newline at end of file From a5d60141b316dd9ba09f15d7578acf5169ddc6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20H=C3=A5land?= Date: Sun, 17 Dec 2023 18:14:36 +0100 Subject: [PATCH 10/13] adaptive card object corrected --- .../ApiClients/ApiModels/Notifications.cs | 3 ++- .../Notifications/ScheduledReportContentBuilderFunction.cs | 2 +- .../Fusion.Resources.Functions.csproj | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/ApiClients/ApiModels/Notifications.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/ApiModels/Notifications.cs index 10ffbf3e4..0082c9792 100644 --- a/src/backend/function/Fusion.Resources.Functions/ApiClients/ApiModels/Notifications.cs +++ b/src/backend/function/Fusion.Resources.Functions/ApiClients/ApiModels/Notifications.cs @@ -1,4 +1,5 @@ using System; +using AdaptiveCards; using Newtonsoft.Json; namespace Fusion.Resources.Functions.ApiClients.ApiModels; @@ -11,5 +12,5 @@ public class SendNotificationsRequest [JsonProperty("description")] public string Description { get; set; } - [JsonProperty("card")] public object Card { get; set; } + [JsonProperty("card")] public AdaptiveCard Card { get; set; } } diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 48c0cb8af..a1c941338 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -188,7 +188,7 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD { Title = $"Weekly summary - {fullDepartment}", EmailPriority = 1, - Card = card, + Card = card.Result, Description = $"Weekly report for department - {fullDepartment}" }, azureUniqueId); diff --git a/src/backend/function/Fusion.Resources.Functions/Fusion.Resources.Functions.csproj b/src/backend/function/Fusion.Resources.Functions/Fusion.Resources.Functions.csproj index eb55b98d5..ea531e0c5 100644 --- a/src/backend/function/Fusion.Resources.Functions/Fusion.Resources.Functions.csproj +++ b/src/backend/function/Fusion.Resources.Functions/Fusion.Resources.Functions.csproj @@ -5,7 +5,7 @@ <_FunctionsSkipCleanOutput>true - + From 3f739daba954c888914816c1d6c75b36fb60e024 Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Sun, 17 Dec 2023 23:33:51 +0100 Subject: [PATCH 11/13] Fixing text for Average time to handle request + Change the way we calculate workload --- .../AdaptiveCards/AdaptiveCardBuilder.cs | 21 +++-- .../ResourceOwnerAdaptiveCardData.cs | 2 +- .../Notifications/Models/PersonnelContent.cs | 1 - .../ScheduledReportContentBuilderFunction.cs | 77 ++++++++++--------- 4 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs index fd3eae07b..83be96ab7 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs @@ -78,14 +78,25 @@ public AdaptiveCardBuilder AddListContainer(string headerText, IEnumerable + { new AdaptiveActionSet() { + Actions = new List + { + new AdaptiveOpenUrlAction() + { + Title = title, + Url = new Uri(url) + } + } + } + } }; - _adaptiveCard.Actions.Add(actionButton); + _adaptiveCard.Body.Add(listContainer); + return this; } public AdaptiveCard Build() diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs index 00178fa00..890d4a1c4 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs @@ -10,7 +10,7 @@ public class ResourceOwnerAdaptiveCardData public int NumberOfOpenRequests { get; set; } public int NumberOfRequestsStartingInMoreThanThreeMonths { get; set; } public int NumberOfRequestsStartingInLessThanThreeMonths { get; set; } - public double AverageTimeToHandleRequests { get; set; } + public string AverageTimeToHandleRequests { get; set; } public int AllocationChangesAwaitingTaskOwnerAction { get; set; } public int ProjectChangesAffectingNextThreeMonths { get; set; } internal IEnumerable PersonnelPositionsEndingWithNoFutureAllocation { get; set; } diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/PersonnelContent.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/PersonnelContent.cs index 5592c2c8b..51a26eab5 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/PersonnelContent.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/PersonnelContent.cs @@ -10,7 +10,6 @@ public class PersonnelContent public string? ProjectName { get; set; } public string? PositionName { get; set; } public double? TotalWorkload { get; set; } - public int? NumberOfPositionInstances { get; set; } public PersonnelPosition? EndingPosition { get; set; } public PersonnelContent() { } diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index a1c941338..3546516ff 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -20,6 +20,7 @@ using Fusion.Integration.Configuration; using Fusion.Integration.ServiceDiscovery; using Microsoft.Extensions.Configuration; +using System.Data; namespace Fusion.Resources.Functions.Functions.Notifications; @@ -147,40 +148,36 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD var averageTimeToHandleRequest = CalculateAverageTimeToHandleRequests(departmentRequests); //8.Allocation changes awaiting task owner action: - //number of allocation changes made by resource owner awaiting task owner action - //M� hente ut alle posisjoner som har ressurser for en gitt avdeling og sjekke p� om det er gjort endringer her den siste tiden var numberOfAllocationchangesAwaitingTaskOwnerAction = GetchangesAwaitingTaskOwnerAction(departmentRequests); //9.Project changes affecting next 3 months - //number of project changes(changes initiated by project / task) with a change affecting the next 3 months var numberOfChangesAffectingNextThreeMonths = GetAllChangesForResourceDepartment(personnelForDepartment); //10.Allocations ending soon with no future allocation var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); //11.Personnel with more than 100 % workload - var listOfPersonnelsWithMoreThan100Percent = personnelForDepartment.Where(p => - p.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum() > 100); - var listOfPersonnelForDepartmentWithMoreThan100Percent = - listOfPersonnelsWithMoreThan100Percent.Select(p => CreatePersonnelWithTBEContent(p)); + var listOfPersonnelWithTBEContent = + personnelForDepartment.Select(p => CreatePersonnelWithTBEContent(p)); + var listOfPersonnelWithTBEContentOnlyMoreThan100PercentWorkload = listOfPersonnelWithTBEContent.Where(p => p.TotalWorkload > 100); var card = ResourceOwnerAdaptiveCardBuilder(new ResourceOwnerAdaptiveCardData - { - TotalNumberOfPersonnel = numberOfPersonnel, - TotalCapacityInUsePercentage = percentageOfTotalCapacity, - NumberOfRequestsLastWeek = numberOfRequestsLastWeek, - NumberOfOpenRequests = totalNumberOfOpenRequests, - NumberOfRequestsStartingInMoreThanThreeMonths = + { + TotalNumberOfPersonnel = numberOfPersonnel, + TotalCapacityInUsePercentage = percentageOfTotalCapacity, + NumberOfRequestsLastWeek = numberOfRequestsLastWeek, + NumberOfOpenRequests = totalNumberOfOpenRequests, + NumberOfRequestsStartingInMoreThanThreeMonths = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, - NumberOfRequestsStartingInLessThanThreeMonths = + NumberOfRequestsStartingInLessThanThreeMonths = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, - AverageTimeToHandleRequests = averageTimeToHandleRequest, - AllocationChangesAwaitingTaskOwnerAction = numberOfAllocationchangesAwaitingTaskOwnerAction, - ProjectChangesAffectingNextThreeMonths = numberOfChangesAffectingNextThreeMonths, - PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, - PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent - }, + AverageTimeToHandleRequests = averageTimeToHandleRequest, + AllocationChangesAwaitingTaskOwnerAction = numberOfAllocationchangesAwaitingTaskOwnerAction, + ProjectChangesAffectingNextThreeMonths = numberOfChangesAffectingNextThreeMonths, + PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, + PersonnelAllocatedMoreThan100Percent = listOfPersonnelWithTBEContentOnlyMoreThan100PercentWorkload + }, fullDepartment, departmentSapId); var sendNotification = await _notificationsClient.SendNotification( @@ -205,9 +202,8 @@ private IEnumerable FilterPersonnelWithoutFutureAllocations( IEnumerable personnelForDepartment) { var threeMonthsFuture = DateTime.UtcNow.AddMonths(3); - var personnelWithPositionsEndingInThreeMonths = personnelForDepartment.Where(x => - x.PositionInstances.Where(pos => pos.IsActive && pos.AppliesTo <= threeMonthsFuture).Any()); + x.PositionInstances.Where(pos => pos.IsActive && pos.AppliesTo <= threeMonthsFuture).Any()); var personnelWithoutFutureAllocations = personnelWithPositionsEndingInThreeMonths.Where(person => person.PositionInstances.All(pos => pos.AppliesTo < threeMonthsFuture)); return personnelWithoutFutureAllocations.Select(p => CreatePersonnelContent(p)); @@ -293,19 +289,20 @@ private int GetchangesAwaitingTaskOwnerAction(IEnumerable listOfRequests) + private string CalculateAverageTimeToHandleRequests(IEnumerable listOfRequests) { - /* How to calculate: - * + /* + * How to calculate: * Find the workflow "created" and then find the date * This should mean that task owner have created and sent the request to resource owner * Find the workflow "proposal" and then find the date * This should mean that the resource owner have done their bit - * TODO: Maybe we need to consider other states - */ + */ + var requestsHandledByResourceOwner = 0; var totalNumberOfDays = 0.0; + var averageTimeUsedToHandleRequest = "NA"; var threeMonthsAgo = DateTime.UtcNow.AddMonths(-3); @@ -333,10 +330,15 @@ private double CalculateAverageTimeToHandleRequests(IEnumerable 0)) - return 0; + if (totalNumberOfDays > 0) + { + var averageAmountOfTimeDouble = totalNumberOfDays / requestsHandledByResourceOwner; + // To get whole number + int averageAmountOfTimeInt = Convert.ToInt32(averageAmountOfTimeDouble); - return totalNumberOfDays / requestsHandledByResourceOwner; + averageTimeUsedToHandleRequest = averageAmountOfTimeInt >= 1 ? averageAmountOfTimeInt.ToString() + " day(s)" : "Less than a day"; + } + return averageTimeUsedToHandleRequest; } private PersonnelContent CreatePersonnelContent(InternalPersonnelPerson person) @@ -359,15 +361,16 @@ private PersonnelContent CreatePersonnelContent(InternalPersonnelPerson person) private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson person) { - var positionInstances = person.PositionInstances.Where(pos => pos.IsActive); - var sumWorkload = positionInstances.Select(pos => pos.Workload).Sum(); - var numberOfPositionInstances = positionInstances.Count(); + var totalWorkLoad = person.ApiPersonAbsences? + .Where(ab => ab.Type != ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); + totalWorkLoad += person.PositionInstances?.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); + var personnelContent = new PersonnelContent() { FullName = person.Name, - TotalWorkload = sumWorkload, - NumberOfPositionInstances = numberOfPositionInstances, + TotalWorkload = totalWorkLoad, }; + return personnelContent; } @@ -388,8 +391,8 @@ private async Task ResourceOwnerAdaptiveCardBuilder(ResourceOwnerA .AddColumnSet(new AdaptiveCardColumn(cardData.NumberOfRequestsStartingInMoreThanThreeMonths.ToString(), "Requests with start date > 3 months")) .AddColumnSet(new AdaptiveCardColumn( - cardData.AverageTimeToHandleRequests.ToString(FormatDoubleToHaveOneDecimal), - "Average time to handle request", "days")) + cardData.AverageTimeToHandleRequests, + "Average time to handle request")) .AddColumnSet(new AdaptiveCardColumn(cardData.AllocationChangesAwaitingTaskOwnerAction.ToString(), "Allocation changes awaiting task owner action")) .AddColumnSet(new AdaptiveCardColumn(cardData.ProjectChangesAffectingNextThreeMonths.ToString(), From ed09d903285b681bd1c0fac76de81d39c464b28a Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Sun, 17 Dec 2023 23:35:35 +0100 Subject: [PATCH 12/13] Small adjustment to method names --- .../ScheduledReportContentBuilderFunction.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 3546516ff..59304dfb2 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -158,7 +158,7 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD //11.Personnel with more than 100 % workload var listOfPersonnelWithTBEContent = - personnelForDepartment.Select(p => CreatePersonnelWithTBEContent(p)); + personnelForDepartment.Select(p => CreatePersonnelContentWithTotalWorkload(p)); var listOfPersonnelWithTBEContentOnlyMoreThan100PercentWorkload = listOfPersonnelWithTBEContent.Where(p => p.TotalWorkload > 100); @@ -206,7 +206,7 @@ private IEnumerable FilterPersonnelWithoutFutureAllocations( x.PositionInstances.Where(pos => pos.IsActive && pos.AppliesTo <= threeMonthsFuture).Any()); var personnelWithoutFutureAllocations = personnelWithPositionsEndingInThreeMonths.Where(person => person.PositionInstances.All(pos => pos.AppliesTo < threeMonthsFuture)); - return personnelWithoutFutureAllocations.Select(p => CreatePersonnelContent(p)); + return personnelWithoutFutureAllocations.Select(p => CreatePersonnelContentWithPositionInformation(p)); } private async Task> GetPersonnelLeave( @@ -341,7 +341,7 @@ private string CalculateAverageTimeToHandleRequests(IEnumerable ab.Type != ApiAbsenceType.Absence && ab.IsActive).Select(ab => ab.AbsencePercentage).Sum(); From b7a3d70d14c7cfa6185520affd947ee2fb0a165b Mon Sep 17 00:00:00 2001 From: Aleksander Lund Date: Mon, 18 Dec 2023 11:29:33 +0100 Subject: [PATCH 13/13] Adjustment to FilterPersonnelWithoutFutureAllocations --- .../ScheduledReportContentBuilderFunction.cs | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs index 59304dfb2..d7342a0d8 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -30,7 +30,6 @@ public class ScheduledReportContentBuilderFunction private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; private readonly IOrgClient _orgClient; - private const string FormatDoubleToHaveOneDecimal = "F1"; private readonly IConfiguration _configuration; public ScheduledReportContentBuilderFunction(ILogger logger, @@ -110,7 +109,6 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD // Adding leave personnelForDepartment = await GetPersonnelLeave(personnelForDepartment); - //1.Number of personnel var numberOfPersonnel = personnelForDepartment.Count(); @@ -132,7 +130,7 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD var numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination = departmentRequests .Count(x => x.OrgPositionInstance != null && x.State != null && - !x.State.Equals("completed") && + !x.State.Equals("completed") && !x.Type.Equals("ResourceOwnerChange") && (x.OrgPositionInstance.AppliesFrom < threeMonthsFuture && x.OrgPositionInstance.AppliesFrom > today) && !x.HasProposedPerson); @@ -141,7 +139,7 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests .Count(x => x.OrgPositionInstance != null && x.State != null && - !x.State.Contains("completed") && + !x.State.Contains("completed") && !x.Type.Equals("ResourceOwnerChange") && x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); //7.Average time to handle request (last 3 months): @@ -201,12 +199,27 @@ private async Task BuildContentForResourceOwner(Guid azureUniqueId, string fullD private IEnumerable FilterPersonnelWithoutFutureAllocations( IEnumerable personnelForDepartment) { - var threeMonthsFuture = DateTime.UtcNow.AddMonths(3); - var personnelWithPositionsEndingInThreeMonths = personnelForDepartment.Where(x => - x.PositionInstances.Where(pos => pos.IsActive && pos.AppliesTo <= threeMonthsFuture).Any()); - var personnelWithoutFutureAllocations = personnelWithPositionsEndingInThreeMonths.Where(person => - person.PositionInstances.All(pos => pos.AppliesTo < threeMonthsFuture)); - return personnelWithoutFutureAllocations.Select(p => CreatePersonnelContentWithPositionInformation(p)); + + var pdList = new List(); + foreach (var pd in personnelForDepartment) + { + var gotLongLastingPosition = pd.PositionInstances.Any(pdi => pdi.AppliesTo >= DateTime.UtcNow.AddMonths(3)); + if (gotLongLastingPosition) + continue; + + var gotFutureAllocation = pd.PositionInstances.Any(pdi => pdi.AppliesFrom > DateTime.UtcNow); + if (gotFutureAllocation) + continue; + var gotActiveAllocation = pd.PositionInstances.Any(pdi => pdi.IsActive); + if (!gotActiveAllocation) + continue; + + pdList.Add(pd); + } + + return pdList.Select(CreatePersonnelContentWithPositionInformation); + + } private async Task> GetPersonnelLeave( @@ -276,7 +289,6 @@ private int GetAllChangesForResourceDepartment(IEnumerable