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 => { 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/ApiClients/IOrgApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/IOrgApiClient.cs new file mode 100644 index 000000000..2b39ef392 --- /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 class ChangeType +{ + 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/ApiClients/IResourcesApiClient.cs b/src/backend/function/Fusion.Resources.Functions/ApiClients/IResourcesApiClient.cs index 14857e765..ad50cdfa2 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 @@ -29,7 +30,13 @@ 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; } + public bool IsDraft { get; set; } } public enum RequestState @@ -41,12 +48,9 @@ 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; } - public enum ApiWorkflowState { Running, Canceled, Error, Completed, Terminated, Unknown } } @@ -55,33 +59,27 @@ public class WorkflowStep { public string Id { get; set; } public string Name { get; set; } - public bool IsCompleted => Completed.HasValue; - /// /// Pending, Approved, Rejected, Skipped /// - [System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))] + [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 } } 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 +96,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 +107,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 +125,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/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/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/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/AdaptiveCard/ResourceOwnerAdaptiveCardData.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCard/ResourceOwnerAdaptiveCardData.cs deleted file mode 100644 index 13abcefda..000000000 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCard/ResourceOwnerAdaptiveCardData.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Collections.Generic; - -namespace Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCard; - -public class ResourceOwnerAdaptiveCardData -{ - public int TotalNumberOfRequests { get; set; } - public int NumberOfOlderRequests { get; set; } - public int NumberOfNewRequestsWithNoNomination { get; set; } - public int NumberOfOpenRequests { get; set; } - internal 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/Models/AdaptiveCards/AdaptiveCardBuilder.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs new file mode 100644 index 000000000..83be96ab7 --- /dev/null +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/AdaptiveCardBuilder.cs @@ -0,0 +1,206 @@ +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 AdaptiveCardBuilder AddActionButton(string title, string url) + { + var listContainer = new AdaptiveContainer + { + Separator = true, + Items = new List + { new AdaptiveActionSet() { + Actions = new List + { + new AdaptiveOpenUrlAction() + { + Title = title, + Url = new Uri(url) + } + } + } + } + }; + + _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/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs new file mode 100644 index 000000000..890d4a1c4 --- /dev/null +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/Models/AdaptiveCards/ResourceOwnerAdaptiveCardData.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace Fusion.Resources.Functions.Functions.Notifications.Models.AdaptiveCards; + +public class ResourceOwnerAdaptiveCardData +{ + public int TotalNumberOfPersonnel { get; set; } + public int TotalCapacityInUsePercentage { get; set; } + public int NumberOfRequestsLastWeek { get; set; } + public int NumberOfOpenRequests { get; set; } + public int NumberOfRequestsStartingInMoreThanThreeMonths { get; set; } + public int NumberOfRequestsStartingInLessThanThreeMonths { get; set; } + public string AverageTimeToHandleRequests { get; set; } + public int AllocationChangesAwaitingTaskOwnerAction { get; set; } + public int ProjectChangesAffectingNextThreeMonths { get; set; } + internal IEnumerable PersonnelPositionsEndingWithNoFutureAllocation { get; set; } + + internal IEnumerable PersonnelAllocatedMoreThan100Percent { get; set; } +} \ No newline at end of file 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 eb2c2bd5c..d7342a0d8 100644 --- a/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs +++ b/src/backend/function/Fusion.Resources.Functions/Functions/Notifications/ScheduledReportContentBuilderFunction.cs @@ -5,16 +5,22 @@ 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.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 Newtonsoft.Json; +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; +using Microsoft.Extensions.Configuration; +using System.Data; namespace Fusion.Resources.Functions.Functions.Notifications; @@ -23,14 +29,19 @@ public class ScheduledReportContentBuilderFunction private readonly ILogger _logger; private readonly INotificationApiClient _notificationsClient; private readonly IResourcesApiClient _resourceClient; + private readonly IOrgClient _orgClient; + private readonly IConfiguration _configuration; public ScheduledReportContentBuilderFunction(ILogger logger, IResourcesApiClient resourcesApiClient, - INotificationApiClient notificationsClient) + INotificationApiClient notificationsClient, + IOrgClient orgClient, IConfiguration configuration) { _logger = logger; _resourceClient = resourcesApiClient; _notificationsClient = notificationsClient; + _orgClient = orgClient; + _configuration = configuration; } [FunctionName("scheduled-report-content-Builder-function")] @@ -54,7 +65,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); @@ -85,82 +96,94 @@ 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 department + var personnelForDepartment = await _resourceClient.GetAllPersonnelForDepartment(fullDepartment); + // Adding leave + personnelForDepartment = await GetPersonnelLeave(personnelForDepartment); + //1.Number of personnel + var numberOfPersonnel = personnelForDepartment.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(); + //2.Capacity in use: + var percentageOfTotalCapacity = FindTotalCapacityIncludingLeave(personnelForDepartment.ToList()); + + // 3.New requests last week (7 days) + 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.State != null && req.Type != null && !req.Type.Equals("ResourceOwnerChange") && + !req.HasProposedPerson && + !req.State.Equals("completed", StringComparison.OrdinalIgnoreCase)); - // 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); - // 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. + //5.Requests with start-date < 3 months var numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination = departmentRequests - .Count(x => !x.State.Contains(RequestState.completed.ToString()) && + .Count(x => x.OrgPositionInstance != null && + x.State != null && + !x.State.Equals("completed") && !x.Type.Equals("ResourceOwnerChange") && (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())); - - - // Get all the personnel for the specific department - var personnelForDepartment = await _resourceClient.GetAllPersonnelForDepartment(fullDepartment); - - //5. List with personnel positions ending within 3 months and with no future allocation (link to personnel allocation) - var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); + //6.Requests with start-date > 3 months: + var numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart = departmentRequests + .Count(x => x.OrgPositionInstance != null && + x.State != null && + !x.State.Contains("completed") && !x.Type.Equals("ResourceOwnerChange") && + x.OrgPositionInstance.AppliesFrom > threeMonthsFuture); - // 6. Number of personnel allocated more than 100 % - 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.Average time to handle request (last 3 months): + var averageTimeToHandleRequest = CalculateAverageTimeToHandleRequests(departmentRequests); + //8.Allocation changes awaiting task owner action: + var numberOfAllocationchangesAwaitingTaskOwnerAction = GetchangesAwaitingTaskOwnerAction(departmentRequests); - //7. % of total allocation vs.capacity - // Show this as a percentagenumber (in the first draft) - var percentageOfTotalCapacity = FindTotalPercentagesAllocatedOfTotal(personnelForDepartment.ToList()); + //9.Project changes affecting next 3 months + var numberOfChangesAffectingNextThreeMonths = GetAllChangesForResourceDepartment(personnelForDepartment); + //10.Allocations ending soon with no future allocation + var listOfPersonnelWithoutFutureAllocations = FilterPersonnelWithoutFutureAllocations(personnelForDepartment); - //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... + //11.Personnel with more than 100 % workload + var listOfPersonnelWithTBEContent = + personnelForDepartment.Select(p => CreatePersonnelContentWithTotalWorkload(p)); + var listOfPersonnelWithTBEContentOnlyMoreThan100PercentWorkload = listOfPersonnelWithTBEContent.Where(p => p.TotalWorkload > 100); var card = ResourceOwnerAdaptiveCardBuilder(new ResourceOwnerAdaptiveCardData - { - NumberOfOlderRequests = numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, - NumberOfOpenRequests = totalNumberOfOpenRequests, - NumberOfNewRequestsWithNoNomination = + { + TotalNumberOfPersonnel = numberOfPersonnel, + TotalCapacityInUsePercentage = percentageOfTotalCapacity, + NumberOfRequestsLastWeek = numberOfRequestsLastWeek, + NumberOfOpenRequests = totalNumberOfOpenRequests, + NumberOfRequestsStartingInMoreThanThreeMonths = + numberOfDepartmentRequestWithMoreThanThreeMonthsBeforeStart, + NumberOfRequestsStartingInLessThanThreeMonths = numberOfDepartmentRequestWithLessThanThreeMonthsBeforeStartAndNoNomination, - NumberOfExtContractsEnding = 0, // TODO: Work in progress... - PersonnelAllocatedMoreThan100Percent = listOfPersonnelForDepartmentWithMoreThan100Percent, - PercentAllocationOfTotalCapacity = percentageOfTotalCapacity, - TotalNumberOfRequests = totalNumberOfRequests, - PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, - }, - fullDepartment); + AverageTimeToHandleRequests = averageTimeToHandleRequest, + AllocationChangesAwaitingTaskOwnerAction = numberOfAllocationchangesAwaitingTaskOwnerAction, + ProjectChangesAffectingNextThreeMonths = numberOfChangesAffectingNextThreeMonths, + PersonnelPositionsEndingWithNoFutureAllocation = listOfPersonnelWithoutFutureAllocations, + PersonnelAllocatedMoreThan100Percent = listOfPersonnelWithTBEContentOnlyMoreThan100PercentWorkload + }, + fullDepartment, departmentSapId); var sendNotification = await _notificationsClient.SendNotification( new SendNotificationsRequest() { Title = $"Weekly summary - {fullDepartment}", EmailPriority = 1, - Card = card, + Card = card.Result, Description = $"Weekly report for department - {fullDepartment}" }, azureUniqueId); @@ -176,250 +199,231 @@ 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 => CreatePersonnelContent(p)); - } - - // Without taking LEAVE into considerations - private int FindTotalPercentagesAllocatedOfTotal(List listOfInternalPersonnel) - { - var totalWorkLoad = 0.0; - foreach (var personnel in listOfInternalPersonnel) + var pdList = new List(); + foreach (var pd in personnelForDepartment) { - totalWorkLoad += personnel.PositionInstances.Where(pos => pos.IsActive).Select(pos => pos.Workload).Sum(); + 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); } - var totalPercentage = totalWorkLoad / (listOfInternalPersonnel.Count * 100) * 100; - - return Convert.ToInt32(totalPercentage); - } + return pdList.Select(CreatePersonnelContentWithPositionInformation); - private PersonnelContent CreatePersonnelContent(InternalPersonnelPerson person) - { - if (person == null) - throw new ArgumentNullException(); - var position = person.PositionInstances.Find(instance => instance.IsActive); - var positionName = position.Name; - var projectName = position.Project.Name; - var personnelContent = new PersonnelContent() - { - FullName = person.Name, - PositionName = positionName, - ProjectName = projectName, - EndingPosition = position - }; - return personnelContent; } - private PersonnelContent CreatePersonnelWithTBEContent(InternalPersonnelPerson person) + private async Task> GetPersonnelLeave( + IEnumerable listOfInternalPersonnel) { - var positionInstances = person.PositionInstances.Where(pos => pos.IsActive); - var sumWorkload = positionInstances.Select(pos => pos.Workload).Sum(); - var numberOfPositionInstances = positionInstances.Count(); - var personnelContent = new PersonnelContent() + List newList = listOfInternalPersonnel.ToList(); + for (int i = 0; i < newList.Count(); i++) { - FullName = person.Name, - TotalWorkload = sumWorkload, - NumberOfPositionInstances = numberOfPositionInstances, - }; - return personnelContent; + var absence = await _resourceClient.GetLeaveForPersonnel(newList[i].AzureUniquePersonId.ToString()); + newList[i].ApiPersonAbsences = absence.ToList(); + } + + listOfInternalPersonnel = newList; + return listOfInternalPersonnel; } - private static AdaptiveCard ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, - string departmentIdentifier) + private int FindTotalCapacityIncludingLeave(List listOfInternalPersonnel) { - var card = new AdaptiveCard(new AdaptiveSchemaVersion(1, 2)); + //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; - card.Body.Add(new AdaptiveTextBlock + foreach (var personnel in listOfInternalPersonnel) { - Text = $"**Weekly summary - {departmentIdentifier}**", - Size = AdaptiveTextSize.Large, - Weight = AdaptiveTextWeight.Bolder, - Wrap = true // Allow text to wrap - }); + 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(); + } - card = CreateAdaptiveCardTemp(card, cardData); + var totalPercentageInludeLeave = totalWorkLoad / ((listOfInternalPersonnel.Count * 100) - totalLeave) * 100; - return card; + return Convert.ToInt32(totalPercentageInludeLeave); } - - // FIXME: Temporary way to compose a adaptive card. Needs refactoring - private static AdaptiveCard CreateAdaptiveCardTemp(AdaptiveCard adaptiveCard, - ResourceOwnerAdaptiveCardData cardData) + private int GetAllChangesForResourceDepartment(IEnumerable listOfInternalPersonnel) { - 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; - } + // 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 - private static AdaptiveCard AddColumnsAndTextToAdaptiveCard(AdaptiveCard adaptiveCard, string customtext1, - string? customtext2, string value) - { - var container = new AdaptiveContainer(); - container.Separator = true; - var columnSet1 = new AdaptiveColumnSet(); + var threeMonths = DateTime.UtcNow.AddMonths(3); + var today = DateTime.UtcNow; - var column1 = new AdaptiveColumn(); - column1.Width = AdaptiveColumnWidth.Stretch; - column1.Separator = true; - column1.Spacing = AdaptiveSpacing.Medium; + var listOfInternalPersonnelwithOnlyActiveProjects = listOfInternalPersonnel.SelectMany(per => + per.PositionInstances.Where(pis => + pis.IsActive || (pis.AppliesFrom < threeMonths && pis.AppliesFrom > today))); - var textBlock1 = new AdaptiveTextBlock(); - textBlock1.Text = customtext1; - textBlock1.Wrap = true; - textBlock1.HorizontalAlignment = AdaptiveHorizontalAlignment.Center; + int totalChangesForDepartment = 0; - var textBlock2 = new AdaptiveTextBlock() + foreach (var instance in listOfInternalPersonnelwithOnlyActiveProjects) { - 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); - + if (instance.Project == null) + continue; + + var changeLogForPersonnel = _orgClient.GetChangeLog(instance.Project.Id.ToString(), + instance.PositionId.ToString(), instance.InstanceId.ToString()); + var totalChanges = changeLogForPersonnel.Result.Events + .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 adaptiveCard; + return totalChangesForDepartment; } - private static AdaptiveCard AddColumnsAndTextToAdaptiveCardForAllocationWithNoFutureAllocations( - AdaptiveCard adaptiveCard, string customtext1, string customtext2, ResourceOwnerAdaptiveCardData carddata) - { - var container = new AdaptiveContainer() - { Separator = true }; + 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(); - 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); + private string 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 + */ - 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 - }; + var requestsHandledByResourceOwner = 0; + var totalNumberOfDays = 0.0; + var averageTimeUsedToHandleRequest = "NA"; + var threeMonthsAgo = DateTime.UtcNow.AddMonths(-3); - column2.Add(textBlock2); - columnset2.Add(column2); + var requestsLastThreeMonthsWithoutResourceOwnerChangeRequest = listOfRequests + .Where(req => req.Created > threeMonthsAgo) + .Where(r => r.Workflow is not null) + .Where(_ => true) + .Where((req => req.Type != null && !req.Type.Equals("ResourceOwnerChange"))); - var column3 = new AdaptiveColumn(); - var textBlock3 = new AdaptiveTextBlock() + foreach (var request in requestsLastThreeMonthsWithoutResourceOwnerChangeRequest) + { + 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; + if (dateForCreation != null && dateForApproval != null) { - 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); + requestsHandledByResourceOwner++; + var timespanDifference = dateForApproval - dateForCreation; + var differenceInDays = timespanDifference.Value.TotalDays; + totalNumberOfDays += differenceInDays; + } } + if (totalNumberOfDays > 0) + { + var averageAmountOfTimeDouble = totalNumberOfDays / requestsHandledByResourceOwner; + // To get whole number + int averageAmountOfTimeInt = Convert.ToInt32(averageAmountOfTimeDouble); - columnSet1.Add(column1); - container.Add(columnSet1); - - adaptiveCard.Body.Add(container); - - return adaptiveCard; + averageTimeUsedToHandleRequest = averageAmountOfTimeInt >= 1 ? averageAmountOfTimeInt.ToString() + " day(s)" : "Less than a day"; + } + return averageTimeUsedToHandleRequest; } - private static AdaptiveCard AddColumnsAndTextToAdaptiveCardForPersonnelWithMoreThan100PercentFTE( - AdaptiveCard adaptiveCard, string customtext1, string customtext2, ResourceOwnerAdaptiveCardData carddata) + private PersonnelContent CreatePersonnelContentWithPositionInformation(InternalPersonnelPerson person) { - var container = new AdaptiveContainer() - { Separator = true }; - - var columnSet1 = new AdaptiveColumnSet(); + if (person == null) + throw new ArgumentNullException(); - var column1 = new AdaptiveColumn(); - var textBlock1 = new AdaptiveTextBlock() + var position = person.PositionInstances.Find(instance => instance.IsActive); + var positionName = position.Name; + var projectName = position.Project.Name; + var personnelContent = new PersonnelContent() { - Text = customtext1, - Wrap = true, - Size = AdaptiveTextSize.Default, - Weight = AdaptiveTextWeight.Bolder + FullName = person.Name, + PositionName = positionName, + ProjectName = projectName, + EndingPosition = position }; - 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 - }; - + return personnelContent; + } - column2.Add(textBlock2); - columnset2.Add(column2); + private PersonnelContent CreatePersonnelContentWithTotalWorkload(InternalPersonnelPerson person) + { + 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 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); - } + var personnelContent = new PersonnelContent() + { + FullName = person.Name, + TotalWorkload = totalWorkLoad, + }; + return personnelContent; + } - columnSet1.Add(column1); - container.Add(columnSet1); + private async Task ResourceOwnerAdaptiveCardBuilder(ResourceOwnerAdaptiveCardData cardData, + string departmentIdentifier, string 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")) + .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, + "Average time to handle request")) + .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", personnelAllocationUri) + .Build(); - adaptiveCard.Body.Add(container); + return card; + } - return adaptiveCard; + private string PortalUri() + { + var portalUri = _configuration["Endpoints_portal"] ?? "https://fusion.equinor.com/"; + if (!portalUri.EndsWith("/")) + portalUri += "/"; + return portalUri; } } \ No newline at end of file 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) 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 - +