using EnVisage.Models; using System; using System.Collections.Generic; using System.Linq; using System.Data.Entity; using System.Web.Mvc; using Prevu.Core.Audit.Model; using EnVisage.Code.Audit; using NLog; namespace EnVisage.Code.BLL { public class ActivityCalendarManager : IDisposable { #region Private Variables private readonly EnVisageEntities _dbContext; private readonly bool _isContexLocal; protected static readonly Logger Logger = LogManager.GetCurrentClassLogger(); #endregion #region Properties private EnVisageEntities DbContext => _dbContext; #endregion #region Constructors public ActivityCalendarManager(EnVisageEntities dbContext) { if (dbContext == null) { _dbContext = new EnVisageEntities(); _isContexLocal = true; } else { _dbContext = dbContext; } } #endregion #region Public Methods public ActivityCalendarSaveModel CopyScenarios(ActivityCalendarSaveModel model, string transactionId) { #if DEBUG var watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif if (model == null) throw new ArgumentNullException(nameof(model)); if (model.Scenarios == null) throw new ArgumentNullException(nameof(model)); if (model.SaveAction != SaveAsScenario.New) throw new InvalidOperationException($"Incorrect operation for Action = {model.SaveAction} state"); if (string.IsNullOrWhiteSpace(model.NewName)) throw new InvalidOperationException("No names for new scenarios specified"); var resultModel = model.Clone(); if (model.Scenarios.Count < 1) return resultModel; // Variables and data for audit EventRequestBaseModel auditRecord; transactionId = Utils.GetOrCreateTransactionId(DbContext, transactionId); var userId = SecurityManager.GetUserPrincipal(); var scenarioManager = new ScenarioManager(DbContext); var scenarioIds = model.Scenarios.Keys.Select(x => new Guid(x)).ToList(); var superExpCatsInScenarios = ScenariosContainsSuperExpenditures(scenarioIds); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios model validation and ScenariosContainsSuperExpenditures {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios model validation and ScenariosContainsSuperExpenditures {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif var scenarios = scenarioManager.GetScenarios(model.Scenarios.Keys.Select(q => new Guid(q)), true).ToList(); var scenarioParentIds = scenarios.Select(w => w.ParentId); var projectAccessByUserExtended = DbContext.VW_ProjectAccessByUserExtended.Where(q => q.UserId == userId && scenarioParentIds.Contains(q.Id)) .GroupBy(t=>t.Id).ToDictionary(key=>key.Key, elem=>elem.FirstOrDefault()); foreach (var scenario in scenarios) { #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios foreach first {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios oreach firsts {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif var scenarioId = scenario.Id; var targetStatus = model.SetActive ? ScenarioStatus.Active : ScenarioStatus.Inactive; var saveType = model.ScenarioType; bool updateProjections = model.Scenarios[scenarioId.ToString()].UpdateProjections; #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios scenarioManager.Loadt {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios scenarioManager.Load {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif var parentProject = projectAccessByUserExtended.ContainsKey(scenario.ParentId.Value) ? projectAccessByUserExtended[scenario.ParentId.Value] : null; if (parentProject == null || !(parentProject.Write > 0)) continue; #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios DbContext.VW_ProjectAccessByUserExtended.FirstOrDefault(x => x.Id == scenario.ParentId) {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios DbContext.VW_ProjectAccessByUserExtended.FirstOrDefault(x => x.Id == scenario.ParentId) {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif // Determine what type of scenario will be set to its copy var scenarioContainsSuperExpenditures = superExpCatsInScenarios.ContainsKey(scenarioId) && superExpCatsInScenarios[scenarioId]; if (scenarioContainsSuperExpenditures && saveType == ScenarioSaveType.BottomUp && scenario.IsBottomUp == false) saveType = ScenarioSaveType.TopDown; // Create copy for scenario var newScenario = scenarioManager.CopyTo(scenarioId, null, targetStatus, false, true, model.NewName, saveType, scenario); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios scenarioManager.CopyTo {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios scenarioManager.CopyTo {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif if ((newScenario == null) || newScenario.Id.Equals(Guid.Empty)) throw new BLLException($"Unable to make a copy of the scenario (id = {scenarioId})"); var newScenarioIdAsText = newScenario.Id.ToString(); if (model.Scenarios.ContainsKey(newScenarioIdAsText)) throw new BLLException($"A copy of the scenario has the same Id as the original one (id = {scenarioId})"); // As a copy of the scenario has new Id, we should move data in the model from source scenario Id to the Id of its copy. // Mark copied scenarios as recently created (isNew <- true) resultModel.Scenarios.Add(newScenarioIdAsText, resultModel.Scenarios[scenarioId.ToString()]); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios resultModel.Scenarios.Addo {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios resultModel.Scenarios.Add {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif resultModel.Scenarios[newScenarioIdAsText].isNew = true; resultModel.Scenarios.Remove(scenarioId.ToString()); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges CopyScenarios resultModel.Scenarios.Remove {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges CopyScenarios resultModel.Scenarios.Remove {watch1.ElapsedMilliseconds} ms"); #endif // Generate audit event records auditRecord = new EventRequestBaseModel { ClassificationKey = "ActivityCalendarSaveAsNewScenario", EntityId = newScenario.Id, ScenarioId = scenarioId, ProjectId = newScenario.ParentId, UserId = userId.ToString(), NewValue = newScenario.Name, Comment = $"Saving from Activity Calendar. New scenario type: '{saveType.ToDisplayValue()}'{(updateProjections ? ". Perform update of Projections" : string.Empty)}" }; AuditProxy.LogEvent(transactionId, auditRecord); } return resultModel; } public void UpdateScenarios(ActivityCalendarSaveModel model, Dictionary expenditureTypes, Dictionary>> resourceTeams, string transactionId) { #if DEBUG var watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif if (model == null) throw new ArgumentNullException(nameof(model)); if (model.Scenarios == null || model.Scenarios.Count < 1) return; // Variables and data for audit EventRequestBaseModel auditRecord; transactionId = Utils.GetOrCreateTransactionId(DbContext, transactionId); var userId = SecurityManager.GetUserPrincipal().ToString(); var scenarioEventsTracker = new ScenarioDetailsEventsTracker(ScenarioDetailsEventsTrackerUsageMode.ActivityCalendar, DbContext, userId); var ecManager = (new ExpenditureCategoryManager(DbContext)); var rateManager = new RateManager(DbContext); var scenarioManager = new ScenarioManager(DbContext); var projectManager = new ProjectManager(DbContext); var teamManager = new TeamManager(DbContext); // Get scenario types and their corresponding actual scenarios var scenarioIds = model.Scenarios.Keys.Select(x => new Guid(x)).ToList(); var scenarioTypes = scenarioManager.GetScenarioTypes(scenarioIds); var actualScenarios = scenarioManager.GetActualScenariosByPortfolioScenarios(scenarioIds); var expenditures = ecManager.GetExpenditureDetails(); var allScenarios = scenarioIds.Union(actualScenarios.Values).Distinct().ToList(); var rates = rateManager.GetRatesDict(allScenarios, rateManager.LoadRatesByScenarios(scenarioIds)); // Teams added to projects as new - for all processed projects var addedTeamsToProjects = new Dictionary>(); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios Get scenario types and their corresponding actual scenarios has taken {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios Get scenario types and their corresponding actual scenarios {watch1.ElapsedMilliseconds} ms"); #endif #if DEBUG var watchAllLoad = new System.Diagnostics.Stopwatch(); var watchTeamAllocation = new System.Diagnostics.Stopwatch(); var watchResourceAllocations = new System.Diagnostics.Stopwatch(); var watchScenarioDetails = new System.Diagnostics.Stopwatch(); var watchActuals = new System.Diagnostics.Stopwatch(); #endif foreach (var scenarioId in scenarioIds) { #if DEBUG watchAllLoad.Start(); #endif if (!scenarioTypes.ContainsKey(scenarioId)) throw new BLLException($"Unable to get ScenarioType for scenario (id = {scenarioId})"); if (!actualScenarios.ContainsKey(scenarioId)) throw new BLLException($"Cannot find actual scenario for the specified scenarioId: {scenarioId}"); // Get scenario data var scenario = scenarioManager.Load(scenarioId, true); if (scenario == null) throw new BLLException($"Scenario not found in the database (id = {scenarioId})"); var parentProject = DbContext.VW_ProjectAccessByUserExtended.FirstOrDefault(x => x.Id == scenario.ParentId); if (parentProject == null || !(parentProject.Write > 0)) continue; var weekEndings = FiscalCalendarManager.GetFiscalCalendarWeeksByRange(scenario.StartDate.Value, scenario.EndDate, DbContext).Select(x => x.EndDate).OrderBy(x => x); var scenarioDataSaveModel = model.Scenarios[scenarioId.ToString()]; var scenarioIsBottomUp = scenarioTypes[scenarioId]; var actualScenarioId = actualScenarios[scenarioId]; #region Generate audit record for scenario saving action if (scenario.ParentId.HasValue) scenarioEventsTracker.StartTracking(scenarioId, scenario.ParentId.Value); if (!scenarioDataSaveModel.isNew) { // Write event. if this scenario was not previously created (by copying of exesting scenario) auditRecord = new EventRequestBaseModel { ClassificationKey = "ActivityCalendarSaveAsCurrentScenario", EntityId = scenarioId, ScenarioId = scenarioId, ProjectId = scenario.ParentId, UserId = userId, OldValue = scenario.Name, Comment = $"Saving from Activity Calendar{(scenarioDataSaveModel.UpdateProjections ? " with update of Projections" : string.Empty)}" }; AuditProxy.LogEvent(transactionId, auditRecord); } #endregion // Find out if data save model has changes of actuals and projections for current scenario (look for manual made changes!) var scenarioHasActualsToUpdate = AreScenarioActualsChanged(scenarioDataSaveModel); var scenarioHasProjectionsToUpdate = AreScenarioProjectionsChanged(scenarioDataSaveModel); var scenarioHasTeamAllocationsToUpdate = AreScenarioTeamAllocationsChanged(scenarioDataSaveModel); var scenarioNewTeams = GetScenarioNewTeams(scenario.ParentId.Value, scenarioDataSaveModel); var scenarioHasNewTeams = scenarioNewTeams != null && (scenarioNewTeams.Count > 0); // Determine scenario recalculation mode var scenarioRecalculationMode = GetScenarioRecalculationMode(model, scenarioId, scenarioIsBottomUp, scenarioHasProjectionsToUpdate, scenarioHasTeamAllocationsToUpdate); #if DEBUG watchAllLoad.Stop(); #endif #if DEBUG watchTeamAllocation.Start(); #endif if (scenarioHasNewTeams) { // Add new teams to scenario projectManager.UpdateProjectTeams(scenario.ParentId.Value, scenarioNewTeams, null, true); CreateTeamAllocations(scenarioId, scenarioDataSaveModel, scenarioNewTeams, weekEndings); addedTeamsToProjects.Add(scenario, scenarioNewTeams); DbContext.ExecuteBulkSaveChanges(); } #if DEBUG watchTeamAllocation.Stop(); #endif #if DEBUG watchResourceAllocations.Start(); #endif if (scenarioHasProjectionsToUpdate) { // Update of resource allocations UpdateResourceAllocations(scenarioId, scenarioDataSaveModel, resourceTeams, scenarioEventsTracker); DbContext.BulkSaveChanges(); } #if DEBUG watchResourceAllocations.Stop(); #endif if (scenarioRecalculationMode != ScenarioRecalculationMode.None) { #if DEBUG watchScenarioDetails.Start(); #endif var scenarioRates = rates.ContainsKey(scenarioId) ? rates[scenarioId] : null; // Update of Team allocations and Details UpdateScenarioAllocations(scenario, scenarioDataSaveModel, scenarioRecalculationMode, expenditureTypes, scenarioRates, weekEndings, scenarioEventsTracker); DbContext.BulkSaveChanges(); #if DEBUG watchScenarioDetails.Stop(); #endif } else { if (scenarioHasTeamAllocationsToUpdate) { // Update of team allocations with no roll-up changes #if DEBUG watchTeamAllocation.Start(); #endif UpdateTeamAllocations(scenarioId, scenarioDataSaveModel, scenarioEventsTracker); DbContext.BulkSaveChanges(); #if DEBUG watchTeamAllocation.Start(); #endif } } #if DEBUG watchActuals.Start(); #endif if (scenarioHasActualsToUpdate) { var actualScenarioRates = rates.ContainsKey(actualScenarioId) ? rates[actualScenarioId] : null; // Update fo resource actuals UpdateResourceActuals(actualScenarioId, scenarioDataSaveModel, actualScenarioRates); DbContext.BulkSaveChanges(); var actualWeekendings = GetScenarioActualsChangedWeekendings(scenarioDataSaveModel); var actualExpenditures = GetScenarioActualsChangedExpenditures(scenarioDataSaveModel); scenarioManager.RecalculateScenarioActuals(actualScenarioId, actualWeekendings, actualExpenditures, expenditures, rates); DbContext.BulkSaveChanges(); } else { scenarioManager.SetBottomUpCosts(scenarioId); DbContext.BulkSaveChanges(); } #if DEBUG watchActuals.Start(); #endif } #if DEBUG System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios All load Summary {watchAllLoad.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateScenarios All load Summary {watchAllLoad.ElapsedMilliseconds} ms"); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios Team Allocations Save Summary {watchTeamAllocation.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateScenarios Team Allocations Save Summary {watchTeamAllocation.ElapsedMilliseconds} ms"); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios Resource Allocations Save Summary {watchResourceAllocations.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateScenarios Resource Allocations Save Summary {watchResourceAllocations.ElapsedMilliseconds} ms"); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios Scenario Details Save Summary {watchScenarioDetails.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateScenarios Scenario Details Save Summary {watchScenarioDetails.ElapsedMilliseconds} ms"); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios Actuals Save Summary {watchActuals.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateScenarios Actuals Save Summary {watchActuals.ElapsedMilliseconds} ms"); #endif // Write audit events for added to projects teams if (addedTeamsToProjects.Count > 0) { var allAddedTeams = addedTeamsToProjects.Values.SelectMany(x => x).Distinct(); var teamNames = teamManager.GetTeamNames(allAddedTeams); if (teamNames != null) { var teamAddEvents = addedTeamsToProjects.Where(x => x.Value != null).SelectMany(x => x.Value.Where(t => teamNames.ContainsKey(t)).Select(t => new EventRequestBaseModel() { ClassificationKey = "ProjectTeamsAdd", EntityId = x.Key.ParentId, ProjectId = x.Key.ParentId, UserId = userId, NewValue = teamNames[t], Comment = String.Format("By '{0}' scenario updates at Activity Calendar", x.Key.Name) })).ToList(); AuditProxy.LogEvent(transactionId, teamAddEvents); } } // Compile audit events and queue events to save var allocationEvents = scenarioEventsTracker.GetEvents(); AuditProxy.LogEvent(transactionId, allocationEvents); scenarioEventsTracker.Dispose(); } public ActivityCalendarFilterOptions GetActivityCalendarFilterOptions(Guid userId) { TeamManager teamMngr = new TeamManager(DbContext); ViewManager viewMngr = new ViewManager(DbContext); CompanyManager cmpnyMngr = new CompanyManager(DbContext); CreditDepartmentManager costCentersMngr = new CreditDepartmentManager(DbContext); ExpenditureCategoryManager expCatsMngr = new ExpenditureCategoryManager(DbContext); var model = new ActivityCalendarFilterOptions(); #region Teams var availableTeams = teamMngr.LoadTeamsWithResourcesByUser(userId); model.Teams = (availableTeams.OrderBy(x => x.Name).Select(x => new SelectListItem() { Text = x.Name, Value = x.Id.ToString() })).ToList(); #endregion #region Views IList availableViews = viewMngr.GetViewsByOwner(userId); model.Views = (availableViews.OrderBy(x => x.Name).Select(x => new SelectListItem() { Text = x.Name, Value = x.Id.ToString() })).ToList(); #endregion #region Companies var availableCompanies = cmpnyMngr.GetCompaniesByUser4Display(userId); model.Companies = new List(); SelectListItem listItem; var companiesRootLevel = availableCompanies.Where(x => !x.ParentCompanyId.HasValue || !availableCompanies.Any(z => z.Id.Equals(x.ParentCompanyId.Value))) .OrderBy(x => x.Name).ToList(); foreach (var rootCompany in companiesRootLevel) { listItem = new SelectListItem { Text = rootCompany.Name, Value = rootCompany.Id.ToString(), Group = null }; model.Companies.Add(listItem); // Get child companies var childCompanies = availableCompanies.Where(x => x.ParentCompanyId.HasValue && x.ParentCompanyId.Value.Equals(rootCompany.Id)) .OrderBy(x => x.Name).ToList(); foreach (var childCompany in childCompanies) { listItem = new SelectListItem() { Text = childCompany.Name, Value = childCompany.Id.ToString(), Group = new SelectListGroup() { Name = rootCompany.Name } }; model.Companies.Add(listItem); } } #endregion #region Cost Centers var availableCostCenters = costCentersMngr.LoadCreditDepartments(); model.CostCenters = this.ConvertCostCentersToOptions(availableCostCenters); #endregion #region Project Roles var availableProjectRoles = expCatsMngr.GetSuperExpenditureDetails(); model.ProjectRoles = this.ConvertExpendituresToOptions(availableProjectRoles.Values.ToList()); #endregion return model; } public ActivityCalendarModel GetActivityCalendarModel(ActivityCalendarRequestModel request) { if (request == null) throw new ArgumentNullException(nameof(request)); // Get teams, related to the incoming page filter. Then get user access permissions for this // team list, and get the only teams, current user has access to. // Then get data for the available to user teams only var userId = SecurityManager.GetUserPrincipal().ToString(); var teamAccessPermissions = GetTeams(userId, request.EntityType, request.EntityId); var teamAccessPermissionsDict = teamAccessPermissions.GroupBy(t => t.TeamId).ToDictionary(gr => gr.Key, grEl => grEl.FirstOrDefault().AccessLevel); var visible2UserTeams = teamAccessPermissionsDict.Keys.ToList(); var fcManager = (new FiscalCalendarManager(DbContext)); var projectManager = (new ProjectManager(DbContext)); var resourcesManager = (new PeopleResourcesManager(DbContext)); var scenarioManager = (new ScenarioManager(DbContext)); var teamManager = (new TeamManager(DbContext)); var ecManager = (new ExpenditureCategoryManager(DbContext)); var fcWeeks = fcManager.GetFiscalCalendar(FiscalCalendarModel.FiscalYearType.Week, request.StartDate, request.EndDate, true, false, true); var weekEndings = fcWeeks.Select(x => x.EndDate).ToList(); if (weekEndings.Count <= 0) return null; var startDate = fcWeeks.Min(x => x.StartDate); var endDate = fcWeeks.Max(x => x.EndDate); var projects = new List(); var filteredByTeamOrView = false; switch (request.EntityType) { case ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource: projects = projectManager.GetProjectsWithChildrenByResource(request.EntityId, includedItems: ProjectManager.LoadProjectItems.Scenarios | ProjectManager.LoadProjectItems.Teams); break; case ActivityCalendarRequestModel.ActivityCalendarEntityType.Company: projects = projectManager.GetProjectsWithChildrenByCompany(request.EntityId, includedItems: ProjectManager.LoadProjectItems.Scenarios | ProjectManager.LoadProjectItems.Teams); break; case ActivityCalendarRequestModel.ActivityCalendarEntityType.View: projects = projectManager.GetProjectsWithChildrenByView(request.EntityId, includedItems: ProjectManager.LoadProjectItems.Scenarios | ProjectManager.LoadProjectItems.Teams); filteredByTeamOrView = true; break; default: projects = projectManager.GetProjectsWithChildrenByTeams(visible2UserTeams, includedItems: ProjectManager.LoadProjectItems.Scenarios | ProjectManager.LoadProjectItems.Teams); filteredByTeamOrView = true; break; } var resources = new List(); List actuals = null; Dictionary resourceActualsTree = null; if (request.EntityType == ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource) { // Get allocations and actuals resources = resourcesManager.GetResourcesWithAllocationsByResourceId(request.EntityId, startDate, endDate); actuals = resourcesManager.GetActualsByResourceId(request.EntityId, startDate, endDate); } else { // Get allocations only. No need to load actuals resources = resourcesManager.GetResourcesWithAllocationsByTeams(visible2UserTeams, startDate, endDate); } var projectsModel = ConvertProjectsToProjectModel(projects, fcWeeks, true).ToDictionary(x => x.ProjectId.ToString()); var scenarios = projectsModel.Select(x => x.Value.ActiveScenario.Id) .ToList(); var resourcesECs = resources.Select(x => x.ExpenditureCategoryId) .Distinct() .ToList(); var superECsInScenarios = scenarioManager.GetExpendituresInScenarios(scenarios, true).Select(x => x.Id); var allECs = resourcesECs.Union(superECsInScenarios).ToList(); var needAllocations = scenarioManager.GetScenarioDetails(scenarios, null, weekEndings); var needAllocationsTree = ConvertNeedAllocationsToTree(needAllocations); var teamAllocations = teamManager.GetTeamsAllocation(visible2UserTeams, scenarios, allECs, startDate, endDate); var teamAllocationsTree = ConvertTeamAllocationsToTree(teamAllocations); var resourceAllocations = resources.SelectMany(x => x.Allocations).ToList(); var resourceAllocationsTree = ConvertResourceAllocationsToTree(resourceAllocations); var restTeamAllocationsTree = new Dictionary(); if (request.EntityType != ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource) { var allUserTeams = teamManager.GetTeamsByUser(Guid.Parse(userId)); var restTeams = allUserTeams.Where(x => !visible2UserTeams.Contains(x.TeamId)).Select(x => x.TeamId).ToList(); var restTeamAllocations = teamManager.GetTeamsAllocation(restTeams, scenarios, allECs, startDate, endDate); restTeamAllocationsTree = GroupTeamAllocationsByScenario(restTeamAllocations); } var unassignedNeed = scenarioManager.GetScenarioNeedUnassigned(scenarios, null, weekEndings, true); var unassignedNeedTree = ConvertUnassignedNeedAllocationsToTree(unassignedNeed); if (actuals != null) { // Convert resource allocations actuals to tree resourceActualsTree = ConvertResourceAllocationsToTree(actuals); } WorkFlowManager wfman = new WorkFlowManager(this.DbContext); List workFlowActions = new List(); foreach (var project in projects) { if (!projectsModel.ContainsKey(project.Id.ToString())) continue; var pModel = projectsModel[project.Id.ToString()]; var actualTeams = project.Teams?.Intersect(visible2UserTeams) ?? new List(); foreach (var teamId in actualTeams) { var assignedResources = resourceAllocations.Where(x => x.ScenarioId == pModel.ActiveScenario.Id && x.TeamId == teamId) .GroupBy(x => x.ExpenditureCategoryId) .ToDictionary(x => x.Key.ToString(), g => g.Select(s => s.PeopleResourceId).Distinct().ToList()); var assignedResourcesModel = new ActivityCalendarProjectTeamWithAssignedResourcesModel() { Resources = assignedResources }; pModel.Teams.Add(teamId.ToString(), assignedResourcesModel); } if (wfman.isProcessExists(pModel.ActiveScenario.Id)) { var wfRecId = pModel.ActiveScenario.Id; var state = wfman.GetCurrentState(wfRecId); pModel.Name = "*" + project.Name + " [" + state.state + "]"; var wfActions = wfman.GetWorkFlowCommands(pModel.ActiveScenario.Id, Guid.Parse(userId)); if (wfActions.Count > 0) { var wfAcCommand = new ActivityCalWorkFlowCommand() { HasChanges = (restTeamAllocationsTree.Keys.All(x => x != wfRecId.ToString())), ProjectName = pModel.Name, ScenarioId = wfRecId, Commands = new List() }; foreach (var wfa in wfActions) { wfAcCommand.Commands.Add(wfa); } if (wfAcCommand.Commands.Count > 0) workFlowActions.Add(wfAcCommand); if (wfActions.Count <= 1) { wfAcCommand.HasChanges = false; } } } } // Get teams with resources var teams = teamManager.GetTeamsInfo(teamAccessPermissions, fcWeeks); if (request.EntityType == ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource) { // We make data model for resource calendar. Keep the current resource info only for every extracted team var resourcesToKeep = new List {request.EntityId}; FilterTeamsByResources(teams, resourcesToKeep); } // Set write permissions to NPT-items SetNonProjectTimeEditPermissions(teams, teamAccessPermissionsDict); // Build Cost centers and Project role lists to update calendar client-side filters var projectRolesInfo = ecManager.GetSuperExpenditureDetails(); var projectRolesInfoTyped = projectRolesInfo.Values.ToList(); var costCenters = GetCostCenters(needAllocations, teamAllocations, projectRolesInfoTyped, filteredByTeamOrView); var costCenterOptions = ConvertCostCentersToOptions(costCenters); var projectRoles = GetProjectRoles(needAllocations, teamAllocations, costCenters, projectRolesInfoTyped); var projectRoleOptions = ConvertExpendituresToOptions(projectRoles); var model = new ActivityCalendarModel() { CostCenters = costCenterOptions, ProjectRoles = projectRoleOptions, WeekEndings = fcManager.CastWeekEndingsToTree(weekEndings), Projects = projectsModel, Teams = teams.ToDictionary(x => x.Id.ToString()), NeedAllocations = needAllocationsTree, TeamAllocations = teamAllocationsTree, ResourceAllocations = resourceAllocationsTree, UnassignedNeed = unassignedNeedTree, ResourceActuals = resourceActualsTree, WorkFlowActions = workFlowActions, RestData = new ActivityCalendarRestData() { TeamAllocations = restTeamAllocationsTree } }; return model; } public bool ActivityCalendarSavePermitted(ActivityCalendarSaveModel model) { if (model.CalendarDisplayMode == ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource) { // Perform special user permissions check for Resource Calendar mode bool resourceProjectionsChanged = model.Scenarios.Values.Any(s => s.Expenditures.Values.Any(e => e.Teams.Values.Any(t => t.Resources.Values.Any(r => r.ForecastAllocations.Any() || r.IsRemoved)))); bool resourceActualsChanged = model.Scenarios.Values.Any(s => s.Expenditures.Values.Any(e => e.Teams.Values.Any(t => t.Resources.Values.Any(r => r.ActualAllocations.Any() || r.IsRemoved)))); bool nptChanged = model.NonProjectTime.Values.Any(n => n.Resources.Values.Any(r => r.Allocations.Any())); var resourceProjectedWritePermission = SecurityManager.CheckSecurityObjectPermission(Areas.RD_ResourceTimeAllocationProjected, AccessLevel.Write); var resourceActualWritePermission = SecurityManager.CheckSecurityObjectPermission(Areas.RD_ResourceTimeAllocationActual, AccessLevel.Write); var resourceAllocationsWriteParentPermission = SecurityManager.CheckSecurityObjectPermission(Areas.RD_ResourceTimeAllocation, AccessLevel.Write); var resourceNptWritePermission = SecurityManager.CheckSecurityObjectPermission(Areas.RD_ResourceNonProjectTime, AccessLevel.Write); if (resourceProjectionsChanged && (!resourceAllocationsWriteParentPermission || !resourceProjectedWritePermission)) // Projections changed, but no permissions for saving projections return false; if (resourceActualsChanged && (!resourceAllocationsWriteParentPermission || !resourceActualWritePermission)) // Actuals changed, but no permissions for saving actuals return false; if (nptChanged && !resourceNptWritePermission) // Non-project Time changed, but no permissions for saving NPT return false; } return true; } public ActivityCalendarSaveModel ValidateCalendarSaveDataModel(ActivityCalendarSaveModel model, Dictionary expenditureTypes, Dictionary>> resourcesTeams) { var modelCopy = model.Clone(); if (resourcesTeams == null) resourcesTeams = new Dictionary>>(); var resources = resourcesTeams.Keys; var availableTeams = (new TeamManager(DbContext)).GetTeamsByUser(SecurityManager.GetUserPrincipal()) .Where(x => x.Write) .Select(x => x.TeamId).ToList(); foreach (var scenario in modelCopy.Scenarios.Values) { foreach (var category in scenario.Expenditures.Values) { var teamsInCategory = category.Teams.Keys.ToList(); foreach (var teamId in teamsInCategory) { if (availableTeams.All(x => x != new Guid(teamId))) category.Teams.Remove(teamId); } foreach (var team in category.Teams.Values) { var resourcesInTeam = team.Resources.Keys.ToList(); foreach (var resourceId in resourcesInTeam) { if (resources.All(x => x != new Guid(resourceId))) team.Resources.Remove(resourceId); } } } } // TODO: Perform checks on model's data // 1) Check model pairs scenario-team fit scenario teams in the database // 2) Check model pairs expenditure-resource fit resource's own expenditure. Mind resource allocations on SuperEC // 3) Check weekendings of the resource allocation in model fit resource's team mebmership dates return modelCopy; } public Dictionary GetExpenditureTypesFromSaveDataModel(ActivityCalendarSaveModel model) { if (model == null) throw new ArgumentNullException(nameof(model)); if ((model.Scenarios == null) || (model.Scenarios.Count < 1)) return new Dictionary(); // Get expenditures from the model var ecMngr = new ExpenditureCategoryManager(DbContext); var modelExpCats = model.Scenarios.Values.SelectMany(s => s.Expenditures.Keys).Distinct() .Select(x => new Guid(x)).ToList(); var databaseExpCats = ecMngr.GetAsDictionary(modelExpCats); var result = databaseExpCats.Values.ToDictionary(x => x.Id, e => e.AllowResourceAssignment); return result; } /// /// Returns team membership dates for specified list of resources /// /// /// Result: D[resourceId, D[teamId, L[TeamMembershipPeriod]]] public Dictionary>> GetTeamsForResourcesFromSaveDataModel(ActivityCalendarSaveModel model) { if (model == null) throw new ArgumentNullException(nameof(model)); // Get resources from model var resources = model.Scenarios.Values.SelectMany(s => s.Expenditures.Values.SelectMany(e => e.Teams.Values.SelectMany(t => t.Resources.Keys))).Distinct() .Select(x => new Guid(x)).ToList(); var prMngr = new PeopleResourcesManager(DbContext); var teamManager = new TeamManager(DbContext); var resourceTeamMembership = prMngr.GetResourcesTeams(resources); var resourceTeamIds = resourceTeamMembership.SelectMany(x => x.Value).Select(x => x.TeamId).Distinct().ToList(); var resourceTeams = teamManager.GetTeamsByUserFiltered(SecurityManager.GetUserPrincipal().ToString(), resourceTeamIds, null, null).ToDictionary(x => x.TeamId); var result = new Dictionary>>(); resourceTeamMembership.Keys.ToList().ForEach(resourceId => { // Resource may be a member of the same team many times in different date periods var teams = resourceTeamMembership[resourceId].Where(x => resourceTeams.ContainsKey(x.TeamId) && resourceTeams[x.TeamId].Write) .GroupBy(x => x.TeamId) .ToDictionary(key => key.Key, grp => grp.Select(rec => new DateRangeNullable() { StartDate = rec.StartDate, EndDate = rec.EndDate }).ToList()); if (teams.Any()) result.Add(resourceId, teams); }); return result; } public Dictionary> GetNewTeamsFromCalendar(ActivityCalendarSaveModel model) { var rt = new Dictionary>(); var ScenarioToTeam = model.Scenarios.ToDictionary(key => key.Key, el => el.Value.Expenditures.Values.SelectMany(e => e.Teams.Keys).Distinct()); foreach (var st in ScenarioToTeam.Keys) { var scenarioId = Guid.Parse(st); var ProjectTeams = (from p in DbContext.Projects join sc in DbContext.Scenarios on p.Id equals sc.ParentId join tp in DbContext.Team2Project on p.Id equals tp.ProjectId into t from subtp in t.DefaultIfEmpty() where sc.Id == scenarioId select new { Id = scenarioId, TeamId = subtp == null ? Guid.Empty : subtp.TeamId }).Distinct().AsNoTracking().ToList(); var teams = ScenarioToTeam[st].Select(Guid.Parse).ToList(); var newTeams = teams.Where(x => !ProjectTeams.Select(y => y.Id.ToString()).ToList().Contains(x.ToString())).ToList(); foreach (var t in newTeams) { var team = this.DbContext.Teams.FirstOrDefault(x => x.Id == t); if (!rt.Keys.Contains(scenarioId)) { var teamList = new List(); teamList.Add(team.Id); rt.Add(scenarioId, teamList); } else { rt[scenarioId].Add(team.Id); } } } return rt; } public void UpdateNonProjectTime(ActivityCalendarSaveModel model) { if (model == null) throw new ArgumentNullException(nameof(model)); if (!model.FilterEntityId.HasValue || model.FilterEntityId.Value.Equals(Guid.Empty)) throw new BLLException("Information on Calendar EntityId filter not specified"); if ((model.NonProjectTime == null) || (model.NonProjectTime.Count < 1)) return; var nptManager = new NonProjectTimeManager(DbContext); var ecManager = new ExpenditureCategoryManager(DbContext); var uomManager = new UOMManager(DbContext); var allExpCats = ecManager.GetAsDictionary(); var allUoms = uomManager.GetAsDictionary(); // Get identifiers for resource-level and team-level NPTs to update var resourceLevelNpts = model.NonProjectTime.Keys.Where(x => model.NonProjectTime[x].Resources.Values.Any()) .Select(x => new Guid(x)).ToList(); var teamLevelNpts = model.NonProjectTime.Keys.Where(x => model.NonProjectTime[x].TeamWideNptAllocations.Any()) .Select(x => new Guid(x)).ToList(); // Get existing NPT-items from the database var allNpts = resourceLevelNpts.Union(teamLevelNpts).ToList(); var nonProjectTimes = DbContext.NonProjectTimes.AsNoTracking().Where(x => allNpts.Contains(x.Id)).ToDictionary(q => q.Id); // Get resources for incoming NPTs and their Expenditures var resIds = model.NonProjectTime.Values.SelectMany(t => t.Resources.Keys) .Select(x => new Guid(x)).Distinct().ToArray(); var resourceExpenditures = DbContext.PeopleResources.Where(t => resIds.Contains(t.Id)).Select(t => new { t.Id, t.ExpenditureCategoryId }) .Distinct().ToDictionary(k => k.Id, el => el.ExpenditureCategoryId); // Get UOMs for teams in team-level NPTs var teamNptUOMs = nptManager.GetNPTUOMs(teamLevelNpts); var resourceAllocations = new Dictionary>(); var teamAllocations = new Dictionary>(); if (model.CalendarDisplayMode == ActivityCalendarRequestModel.ActivityCalendarEntityType.Team) { // Get NPT allocations for Calendar in TEAM display mode resourceAllocations = nptManager.GetNPTResourceAllocationsByTeam(allNpts, model.FilterEntityId.Value); teamAllocations = nptManager.GetNPTTeamAllocationsByTeam(allNpts, model.FilterEntityId.Value); } else if (model.CalendarDisplayMode == ActivityCalendarRequestModel.ActivityCalendarEntityType.View) { // Get NPT allocations for Calendar in VIEW display mode resourceAllocations = nptManager.GetNPTResourceAllocationsByView(allNpts, model.FilterEntityId.Value); teamAllocations = nptManager.GetNPTTeamAllocationsByView(allNpts, model.FilterEntityId.Value); } else if (model.CalendarDisplayMode == ActivityCalendarRequestModel.ActivityCalendarEntityType.Company) { // Get NPT allocations for Calendar in COMPANY display mode resourceAllocations = nptManager.GetNPTResourceAllocationsByCompany(allNpts, model.FilterEntityId.Value); teamAllocations = nptManager.GetNPTTeamAllocationsByCompany(allNpts, model.FilterEntityId.Value); } else if (model.CalendarDisplayMode == ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource) { // Get NPT allocations for Calendar in RESOURCE display mode resourceAllocations = nptManager.GetNPTResourceAllocations(allNpts, model.FilterEntityId.Value); } foreach (var nptIdAsText in model.NonProjectTime.Keys) { var nptId = new Guid(nptIdAsText); var nptModel = model.NonProjectTime[nptIdAsText]; NonProjectTime nonProjectTime; nonProjectTimes.TryGetValue(nptId, out nonProjectTime); if (nonProjectTime.Id == Guid.Empty) throw new BLLException($"Non-project time item not found in the database (Id = {nptIdAsText})"); if (!nonProjectTime.IsTeamMode) { #region Update Resource NPT allocations foreach (var resourceIdAsText in nptModel.Resources.Keys) { var resourceId = new Guid(resourceIdAsText); var resourceNptModel = nptModel.Resources[resourceIdAsText]; if (!resourceAllocations.ContainsKey(nptId)) continue; // All NPT for resource var resourceNptAllocations = resourceAllocations[nptId].Where(x => x.NonProjectTime2Resource.PeopleResourceId == resourceId); foreach (var weekendingsAsText in resourceNptModel.Allocations.Keys) { var weekending = Utils.ConvertFromUnixDate(Convert.ToInt64(weekendingsAsText)); var newVal = resourceNptModel.Allocations[weekendingsAsText]; UOM uom = null; var oldAllocations = resourceNptAllocations.Where(x => x.WeekEndingDate == weekending).ToList(); if (oldAllocations.Count > 0) { if (allExpCats.Keys.Contains(resourceExpenditures[resourceId])) { if (allUoms.ContainsKey(allExpCats[resourceExpenditures[resourceId]].UOMId)) uom = allUoms[allExpCats[resourceExpenditures[resourceId]].UOMId]; } var uomValue = uom?.UOMValue; var coef = newVal == 0 ? 0 : oldAllocations.Select(x => x.HoursOff).Sum() / newVal; foreach (var val in oldAllocations) { // Update NPT resource allocation var nptAllocation = oldAllocations.FirstOrDefault(); if (nptAllocation != null) { var value = nptAllocation.HoursOff == 0 || coef == 0 ? newVal : Convert.ToDecimal(nptAllocation.HoursOff) / coef; nptAllocation.HoursOff = Convert.ToInt32(uomValue.HasValue && uomValue.Value < value ? uomValue.Value : value); } } } } } #endregion } else { #region Update Team NPT allocations foreach (var weekendingsAsText in nptModel.TeamWideNptAllocations.Keys) { var weekending = Utils.ConvertFromUnixDate(Convert.ToInt64(weekendingsAsText)); var newVal = nptModel.TeamWideNptAllocations[weekendingsAsText]; if (!teamAllocations.ContainsKey(nptId)) continue; var uomValue = (decimal?)null; var nptAllocations = teamAllocations[nptId]; var oldAllocations = nptAllocations.Where(x => x.WeekEndingDate == weekending).ToList(); if (oldAllocations.Count > 0) { if (teamNptUOMs.ContainsKey(nptId)) uomValue = teamNptUOMs[nptId]; var coef = newVal == 0 ? 0 : (oldAllocations.Select(x => x.HoursOff).Sum() / oldAllocations.Count) / newVal; foreach (var nptAllocation in oldAllocations) { var newValue = nptAllocation.HoursOff == 0 || coef == 0 ? newVal : Convert.ToDecimal(nptAllocation.HoursOff) / coef; nptAllocation.HoursOff = Convert.ToInt32(uomValue.HasValue && (uomValue < newValue) ? uomValue : newValue); } } } #endregion } } } #endregion #region Convert Methods public List ConvertProjectsToProjectModel(List projects, List fcWeeks, bool onlyActiveScenario) { if (projects == null || projects.Count <= 0) return new List(); if (fcWeeks == null || fcWeeks.Count <= 0) return new List(); var result = new List(); var defaultProjectColor = Utils.GetDefaultProjectColor(); var startDate = fcWeeks.Min(x => x.EndDate); var endDate = fcWeeks.Max(x => x.EndDate); foreach (var project in projects) { if (project.HasChildren) continue; var activeScenario = project.Scenarios.OrderByDescending(x => x.DateCreated) .FirstOrDefault(x => x.ParentId == project.Id && (!onlyActiveScenario || x.Status == ScenarioStatus.Active) && x.Type == ScenarioType.Portfolio && x.StartDate.HasValue && x.StartDate.Value.Date <= endDate && x.EndDate.HasValue && x.EndDate.Value.Date >= startDate); if (activeScenario == null) continue; var nearestFiscalWeek = fcWeeks.FirstOrDefault(x => x.EndDate >= activeScenario.EndDate.Value); var nearestWeekEnding = nearestFiscalWeek != null ? nearestFiscalWeek.EndDate : activeScenario.EndDate.Value; var inactiveScenarios = project.Scenarios.Where(x => x.ParentId == project.Id && x.Type == ScenarioType.Portfolio && x.StartDate.HasValue && x.EndDate.HasValue && x.Status == (int)ScenarioStatus.Inactive).ToList(); var model = new ActivityCalendarProjectModel() { ProjectId = project.Id, ParentProjectId = project.ParentProject != null ? project.ParentProject.Id : project.Id, ParentName = project.ParentProject != null ? project.ParentProject.Name : null, PartId = project.ParentProject != null ? (Guid?)project.Id : null, // in any case we must have company id on current project or its parent, otherwise set Guid.Empty to see the corrupted data CompanyId = (project.ParentProject != null ? project.ParentProject.CompanyId : project.CompanyId) ?? Guid.Empty, // client is always related to the project (in case of simple project) or to the project part (in case of project with parts) ClientId = project.ClientId ?? Guid.Empty, Name = project.Name, TypeName = project.TypeName, DeadlineMs = project.Deadline.HasValue ? Utils.ConvertToUnixDate(project.Deadline.Value) : 0, Priority = project.Priority, Color = Utils.GetProjectColor(project, defaultProjectColor), ReadOnly = project.ReadOnly, ActiveScenario = new ActivityCalendarScenarioModel { Id = activeScenario.Id, Name = activeScenario.Name, IsBottomUp = activeScenario.IsBottomUp, StartDate = Utils.ConvertToUnixDate(activeScenario.StartDate.Value), EndDate = Utils.ConvertToUnixDate(nearestWeekEnding), }, InactiveScenarios = inactiveScenarios.Select(x => new ActivityCalendarScenarioModel() { Id = x.Id, Name = x.Name, IsBottomUp = x.IsBottomUp, StartDate = Utils.ConvertToUnixDate(x.StartDate.Value), // we use this parameter only for dependency widget, so we may use it as is, without calculation of nearest week ending EndDate = Utils.ConvertToUnixDate(x.EndDate.Value), }).ToList(), }; result.Add(model); } return result; } public Dictionary ConvertNeedAllocationsToTree(List allocations) { if (allocations == null || allocations.Count <= 0) return new Dictionary(); var tree = new Dictionary(); foreach (var allocation in allocations) { if (!allocation.ParentID.HasValue || !allocation.WeekEndingDate.HasValue) continue; var scenarioKey = allocation.ParentID.Value.ToString(); var ecKey = allocation.ExpenditureCategoryId.ToString(); var weekEndingKey = Utils.ConvertToUnixDate(allocation.WeekEndingDate.Value).ToString(); if (!tree.ContainsKey(scenarioKey)) tree.Add(scenarioKey, new ActivityCalendarAllocationsByECModel()); if (!tree[scenarioKey].Expenditures.ContainsKey(ecKey)) tree[scenarioKey].Expenditures.Add(ecKey, new ActivityCalendarAllocationsModel()); tree[scenarioKey].Expenditures[ecKey].Allocations.Add(weekEndingKey, allocation.Quantity ?? 0); } return tree; } public Dictionary ConvertTeamAllocationsToTree(List allocations) { if (allocations == null || allocations.Count <= 0) return new Dictionary(); var tree = new Dictionary(); foreach (var allocation in allocations) { var teamKey = allocation.TeamId.ToString(); var scenarioKey = allocation.ScenarioId.ToString(); var ecKey = allocation.ExpenditureCategoryId.ToString(); var weekEndingKey = Utils.ConvertToUnixDate(allocation.WeekEndingDate).ToString(); if (!tree.ContainsKey(teamKey)) tree.Add(teamKey, new ActivityCalendarTeamAllocationsModel()); if (!tree[teamKey].Scenarios.ContainsKey(scenarioKey)) tree[teamKey].Scenarios.Add(scenarioKey, new ActivityCalendarAllocationsByECModel()); if (!tree[teamKey].Scenarios[scenarioKey].Expenditures.ContainsKey(ecKey)) tree[teamKey].Scenarios[scenarioKey].Expenditures.Add(ecKey, new ActivityCalendarAllocationsModel()); tree[teamKey].Scenarios[scenarioKey].Expenditures[ecKey].Allocations.Add(weekEndingKey, allocation.Quantity); } return tree; } public Dictionary GroupTeamAllocationsByScenario(List allocations) { if (allocations == null || allocations.Count <= 0) return new Dictionary(); var tree = new Dictionary(); foreach (var allocation in allocations) { var scenarioKey = allocation.ScenarioId.ToString(); var ecKey = allocation.ExpenditureCategoryId.ToString(); var weekEndingKey = Utils.ConvertToUnixDate(allocation.WeekEndingDate).ToString(); if (!tree.ContainsKey(scenarioKey)) tree.Add(scenarioKey, new ActivityCalendarAllocationsByECModel()); if (!tree[scenarioKey].Expenditures.ContainsKey(ecKey)) tree[scenarioKey].Expenditures.Add(ecKey, new ActivityCalendarAllocationsModel()); if (!tree[scenarioKey].Expenditures[ecKey].Allocations.ContainsKey(weekEndingKey)) tree[scenarioKey].Expenditures[ecKey].Allocations.Add(weekEndingKey, 0); tree[scenarioKey].Expenditures[ecKey].Allocations[weekEndingKey] += allocation.Quantity; } return tree; } public Dictionary ConvertUnassignedNeedAllocationsToTree(List allocations) { if (allocations == null || allocations.Count <= 0) return new Dictionary(); var tree = new Dictionary(); foreach (var allocation in allocations) { if (!allocation.WeekEndingDate.HasValue) continue; var scenarioKey = allocation.ScenarioId.ToString(); var ecKey = allocation.ExpenditureCategoryId.ToString(); var weekEndingKey = Utils.ConvertToUnixDate(allocation.WeekEndingDate.Value).ToString(); if (!tree.ContainsKey(scenarioKey)) tree.Add(scenarioKey, new ActivityCalendarAllocationsByECModel()); if (!tree[scenarioKey].Expenditures.ContainsKey(ecKey)) tree[scenarioKey].Expenditures.Add(ecKey, new ActivityCalendarAllocationsModel()); tree[scenarioKey].Expenditures[ecKey].Allocations.Add(weekEndingKey, allocation.RemainingQuantity); } return tree; } #endregion #region Private Methods private Dictionary ConvertResourceAllocationsToTree(List allocations) { if (allocations == null || allocations.Count <= 0) return new Dictionary(); var tree = new Dictionary(); foreach (var allocation in allocations) { var resourceKey = allocation.PeopleResourceId.ToString(); var scenarioKey = allocation.ScenarioId.ToString(); var teamKey = allocation.TeamId.ToString(); var ecKey = allocation.ExpenditureCategoryId.ToString(); var weekEndingKey = Utils.ConvertToUnixDate(allocation.WeekEndingDate).ToString(); if (!tree.ContainsKey(resourceKey)) tree.Add(resourceKey, new ActivityCalendarPeopleResourceAllocationsByScenarioModel()); if (!tree[resourceKey].Scenarios.ContainsKey(scenarioKey)) tree[resourceKey].Scenarios.Add(scenarioKey, new ActivityCalendarPeopleResourceAllocationsByTeamModel()); if (!tree[resourceKey].Scenarios[scenarioKey].Teams.ContainsKey(teamKey)) tree[resourceKey].Scenarios[scenarioKey].Teams.Add(teamKey, new ActivityCalendarAllocationsByECModel()); if (!tree[resourceKey].Scenarios[scenarioKey].Teams[teamKey].Expenditures.ContainsKey(ecKey)) tree[resourceKey].Scenarios[scenarioKey].Teams[teamKey].Expenditures.Add(ecKey, new ActivityCalendarAllocationsModel()); tree[resourceKey].Scenarios[scenarioKey].Teams[teamKey].Expenditures[ecKey].Allocations.Add(weekEndingKey, allocation.Quantity); } return tree; } private List GetTeams(string userId, ActivityCalendarRequestModel.ActivityCalendarEntityType entityType, Guid entityId) { if (entityType == 0 || entityId == Guid.Empty) return new List(); var teams = new List(); var teamManager = new TeamManager(DbContext); switch (entityType) { case ActivityCalendarRequestModel.ActivityCalendarEntityType.Team: teams = teamManager.GetTeamsByUserFiltered(userId, new List() { entityId }, null, null); break; case ActivityCalendarRequestModel.ActivityCalendarEntityType.View: teams = teamManager.GetTeamsByUserFiltered(userId, null, new List() { entityId }, null); break; // for filtering by company we need to show all teams that are related to selected company case ActivityCalendarRequestModel.ActivityCalendarEntityType.Company: teams = teamManager.GetTeamsByCompanies(new Guid(userId), new List() { entityId }); break; case ActivityCalendarRequestModel.ActivityCalendarEntityType.Resource: teams = teamManager.GetTeamsByResources(userId, new List() { entityId }); break; } return teams; } private void CreateTeamAllocations(Guid scenarioId, ActivityCalendarSaveModel.ScenarioSaveModel model, List newTeams, IEnumerable weekEndings) { if (scenarioId == Guid.Empty) throw new ArgumentOutOfRangeException("scenarioId"); if (model == null) throw new ArgumentNullException("model"); if (newTeams == null || !newTeams.Any()) throw new ArgumentNullException("newTeams"); if (weekEndings == null || !weekEndings.Any()) throw new ArgumentNullException("weekEndings"); if (model.Expenditures == null || !model.Expenditures.Any()) return; var teamAllocationCollection = new List(); foreach (var teamId in newTeams) { foreach (var category in model.Expenditures) { if (category.Value.Teams == null || !category.Value.Teams.ContainsKey(teamId.ToString())) continue; foreach (var weekEnding in weekEndings) { var allocation = new TeamAllocation() { Id = Guid.NewGuid(), ScenarioId = scenarioId, TeamId = teamId, ExpenditureCategoryId = Guid.Parse(category.Key), Quantity = 0, WeekEndingDate = weekEnding }; teamAllocationCollection.Add(allocation); } } } DbContext.TeamAllocations.AddRange(teamAllocationCollection); } private void UpdateResourceAllocations(Guid scenarioId, ActivityCalendarSaveModel.ScenarioSaveModel model, Dictionary>> resourceTeams, ScenarioDetailsEventsTracker tracker = null) { #if DEBUG var watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif // Resources, that have changed allocations var resourcesWithChangedAllocations = model.Expenditures.Values.SelectMany(e => e.Teams.Values.SelectMany(t => t.Resources.Where(r => (r.Value.ForecastAllocations.Count > 0) || r.Value.IsRemoved) .Select(r => new Guid(r.Key)))).ToList(); // Get existing resource allocations from DB var resourceAllocations = DbContext.PeopleResourceAllocations.Where(x => x.ScenarioId.Equals(scenarioId) && resourcesWithChangedAllocations.Contains(x.PeopleResourceId)) .GroupBy(x => x.ExpenditureCategoryId).ToDictionary(e => e.Key, eGrp => eGrp.GroupBy(x => x.TeamId).ToDictionary(t => t.Key, tGrp => tGrp.GroupBy(x => x.PeopleResourceId).ToDictionary(r => r.Key, rGrp => rGrp.GroupBy(x => x.WeekEndingDate).ToDictionary(w => w.Key, x => x.FirstOrDefault())))); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios UpdateResourceAllocations DbContext.PeopleResourceAllocations.Where(x => {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateResourceAllocations DbContext.PeopleResourceAllocations.Where(x => {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif var peopleResourceAllocationToAdd = new List(); var peopleResourceAllocationToDelete= new List(); foreach (var expCatIdAsText in model.Expenditures.Keys) { var expCatId = new Guid(expCatIdAsText); var expCatModel = model.Expenditures[expCatIdAsText]; if (!resourceAllocations.ContainsKey(expCatId)) resourceAllocations.Add(expCatId, new Dictionary>>()); foreach (var teamIdAsText in expCatModel.Teams.Keys) { var teamId = new Guid(teamIdAsText); var teamModel = expCatModel.Teams[teamIdAsText]; if (!resourceAllocations[expCatId].ContainsKey(teamId)) resourceAllocations[expCatId].Add(teamId, new Dictionary>()); foreach (var resourceIdAsText in teamModel.Resources.Keys) { var resourceId = new Guid(resourceIdAsText); var resourceModel = teamModel.Resources[resourceIdAsText]; if (!resourceAllocations[expCatId][teamId].ContainsKey(resourceId)) resourceAllocations[expCatId][teamId].Add(resourceId, new Dictionary()); var currentResourceAllocations = resourceAllocations[expCatId][teamId][resourceId]; if (resourceModel.IsRemoved) { // Remove allocations for resource if (currentResourceAllocations.Count > 0) { // Log changes for audit events if (tracker != null) tracker.ResourceAllocationValuesRemoved(currentResourceAllocations.Values); //currentResourceAllocations.Values.ToList().ForEach(x => DbContext.Entry(x).State = EntityState.Deleted); peopleResourceAllocationToDelete.AddRange(currentResourceAllocations.Values); resourceAllocations[expCatId][teamId][resourceId].Clear(); } } else { // Perform update or create for resource allocations and actuals foreach (var weekendingAsText in resourceModel.ForecastAllocations.Keys) { // Get allocations to update var weekending = Utils.ConvertFromUnixDate(Convert.ToInt64(weekendingAsText)); PeopleResourceAllocation allocationItem; decimal newValue = resourceModel.ForecastAllocations[weekendingAsText] > 0 ? resourceModel.ForecastAllocations[weekendingAsText] : 0; // Check resource allocation is out of the resource team membership dates bool isAllocationCorrect = (resourceTeams != null) && resourceTeams.ContainsKey(resourceId) && resourceTeams[resourceId].ContainsKey(teamId) && resourceTeams[resourceId][teamId].Any(x => x.IsInRange(weekending)); if (!currentResourceAllocations.ContainsKey(weekending)) { if (isAllocationCorrect) { // Create new allocation record, if it is correct allocationItem = new PeopleResourceAllocation() { Id = Guid.NewGuid(), ScenarioId = scenarioId, ExpenditureCategoryId = expCatId, TeamId = teamId, PeopleResourceId = resourceId, WeekEndingDate = weekending, Quantity = newValue }; peopleResourceAllocationToAdd.Add(allocationItem); resourceAllocations[expCatId][teamId][resourceId].Add(weekending, allocationItem); // Log changes for audit events if (tracker != null) tracker.ResourceAllocationValueAdded(allocationItem); } } else { if (isAllocationCorrect) { // Update existing allocation record allocationItem = currentResourceAllocations[weekending]; var oldValue = allocationItem.Quantity; allocationItem.Quantity = newValue; //DbContext.Entry(allocationItem).State = EntityState.Modified; // Log changes for audit events if (tracker != null) tracker.ResourceAllocationValueChanged(allocationItem, oldValue); } else { // Allocations is incorrect. Remove record from the database allocationItem = currentResourceAllocations[weekending]; //DbContext.Entry(allocationItem).State = EntityState.Deleted; peopleResourceAllocationToDelete.Add(allocationItem); // Log changes for audit events if (tracker != null) tracker.ResourceAllocationValueRemoved(allocationItem); } } } } } } } #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios UpdateResourceAllocations foreach (var expCatIdAsText in model.Expenditures.Keys) {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateResourceAllocations foreach (var expCatIdAsText in model.Expenditures.Keys) {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif DbContext.PeopleResourceAllocations.AddRange(peopleResourceAllocationToAdd); DbContext.PeopleResourceAllocations.RemoveRange(peopleResourceAllocationToDelete); DbContext.BulkSaveChanges(); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"SaveChanges UpdateScenarios UpdateResourceAllocations DbContext.BulkSaveChanges(); {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"SaveChanges UpdateScenarios UpdateResourceAllocations DbContext.BulkSaveChanges(); {watch1.ElapsedMilliseconds} ms"); #endif } private void UpdateResourceActuals(Guid actualScenarioId, ActivityCalendarSaveModel.ScenarioSaveModel model, Dictionary> rates) { // Resources, that have changed actuals var resourcesWithChangedActuals = model.Expenditures.Values.SelectMany(e => e.Teams.Values.SelectMany(t => t.Resources.Where(r => r.Value.ActualAllocations.Count > 0).Select(r => new Guid(r.Key)))).ToList(); // Get existing resource actuals and details actuals from DB var databaseActuals = (from pra in DbContext.PeopleResourceActuals join sda in DbContext.ScenarioDetail on pra.ParentId equals sda.Id where resourcesWithChangedActuals.Contains(pra.PeopleResourceId) && sda.ParentID == actualScenarioId select new { resourceActuals = pra, detailsActuals = sda }).Distinct().ToList(); // D[ExpCatId, D[ResourceId, D[Weekending, PeopleResourceActual]]] var resourceActuals = databaseActuals.Where(x => x.detailsActuals.ExpenditureCategoryId.HasValue && x.detailsActuals.WeekEndingDate.HasValue) .GroupBy(x => x.detailsActuals.ExpenditureCategoryId.Value).ToDictionary(e => e.Key, eGrp => eGrp.GroupBy(x => x.resourceActuals.PeopleResourceId).ToDictionary(r => r.Key, rGrp => rGrp.GroupBy(x => x.detailsActuals.WeekEndingDate.Value).ToDictionary(w => w.Key, x => x.Select(a => a.resourceActuals).FirstOrDefault()))); // D[ExpCatId, D[Weekending, ScenarioDetail]] var detailsActuals = databaseActuals.Select(x => x.detailsActuals) .Where(x => x.ExpenditureCategoryId.HasValue && x.WeekEndingDate.HasValue).Distinct() .GroupBy(x => x.ExpenditureCategoryId.Value).ToDictionary(e => e.Key, eGrp => eGrp.GroupBy(x => x.WeekEndingDate.Value).ToDictionary(w => w.Key, sd => sd.FirstOrDefault())); databaseActuals.Clear(); #region Update resource actuals var scenarioDetailsToAdd = new List(); var peopleResourceActualsToAdd = new List(); foreach (var expCatIdAsText in model.Expenditures.Keys) { var expCatId = new Guid(expCatIdAsText); var expCat = model.Expenditures[expCatIdAsText]; if (!resourceActuals.ContainsKey(expCatId)) resourceActuals.Add(expCatId, new Dictionary>()); if (!detailsActuals.ContainsKey(expCatId)) detailsActuals.Add(expCatId, new Dictionary()); foreach (var teamIdAsText in expCat.Teams.Keys) { var team = expCat.Teams[teamIdAsText]; foreach (var resourceIdAsText in team.Resources.Keys) { var resourceId = new Guid(resourceIdAsText); var resource = team.Resources[resourceIdAsText]; if (!resourceActuals[expCatId].ContainsKey(resourceId)) resourceActuals[expCatId].Add(resourceId, new Dictionary()); var currentResourceActuals = resourceActuals[expCatId][resourceId]; if (!resource.IsRemoved) { // Perform update or create for resource allocations and actuals foreach (var weekendingAsText in resource.ActualAllocations.Keys) { var weekending = Utils.ConvertFromUnixDate(Convert.ToInt64(weekendingAsText)); decimal newValue = resource.ActualAllocations[weekendingAsText] > 0 ? resource.ActualAllocations[weekendingAsText] : 0; // Check existance of ScenarioDetail item for resource actuals records if (!detailsActuals[expCatId].ContainsKey(weekending)) { // Create ScenarioDetails item var detailsItem = new ScenarioDetail() { Id = Guid.NewGuid(), ParentID = actualScenarioId, ExpenditureCategoryId = expCatId, WeekEndingDate = weekending, WeekOrdinal = 0, Quantity = 0, // Will be calculated later Cost = 0 // Will be calculated later }; scenarioDetailsToAdd.Add(detailsItem); detailsActuals[expCatId].Add(weekending, detailsItem); } var parentScenarioDetailsItemId = detailsActuals[expCatId][weekending].Id; PeopleResourceActual actualItem = null; if (!currentResourceActuals.ContainsKey(weekending)) { // Create new allocation record actualItem = new PeopleResourceActual() { Id = Guid.NewGuid(), ParentId = parentScenarioDetailsItemId, PeopleResourceId = resourceId, Quantity = newValue, Cost = newValue * RateManager.GetRateValue(rates, expCatId, weekending), RowCreateDate = DateTime.UtcNow }; peopleResourceActualsToAdd.Add(actualItem); resourceActuals[expCatId][resourceId].Add(weekending, actualItem); } else { // Update existing resource actuals record actualItem = currentResourceActuals[weekending]; actualItem.Quantity = newValue; actualItem.Cost = newValue * RateManager.GetRateValue(rates, expCatId, weekending); DbContext.Entry(actualItem).State = EntityState.Modified; } } } } } DbContext.ScenarioDetail.AddRange(scenarioDetailsToAdd); DbContext.PeopleResourceActuals.AddRange(peopleResourceActualsToAdd); DbContext.BulkSaveChanges(); } #endregion #region Recalculate Details actuals foreach (var expCatIdAsText in model.Expenditures.Keys) { var expCatId = new Guid(expCatIdAsText); var expCat = model.Expenditures[expCatIdAsText]; var weekendingsToRecalculate = expCat.Teams.Values.SelectMany(t => t.Resources.Values.Where(r => !r.IsRemoved).SelectMany(a => a.ActualAllocations.Keys)) .Distinct().Select(x => Utils.ConvertFromUnixDate(Convert.ToInt64(x))).OrderBy(x => x).ToList(); foreach (var we in weekendingsToRecalculate) { if (detailsActuals.ContainsKey(expCatId) && detailsActuals[expCatId].ContainsKey(we)) { var detailsItem = detailsActuals[expCatId][we]; var foundResourceActualsRecords = new List(); decimal newQuantityValue = 0; decimal newCostValue = 0; if (resourceActuals.ContainsKey(expCatId)) { foundResourceActualsRecords = resourceActuals[expCatId].Values .Where(r => r.ContainsKey(we)).Select(a => a[we]).ToList(); } if (foundResourceActualsRecords.Count > 0) { newQuantityValue = foundResourceActualsRecords.Sum(x => x.Quantity); newCostValue = foundResourceActualsRecords.Sum(x => x.Cost); } detailsItem.Quantity = newQuantityValue > 0 ? newQuantityValue : 0; detailsItem.Cost = newCostValue; } } } #endregion } private void UpdateTeamAllocations(Guid scenarioId, ActivityCalendarSaveModel.ScenarioSaveModel model, ScenarioDetailsEventsTracker tracker = null) { // Teams, that have changed allocations var teamsWithChangedAllocations = model.Expenditures.Values.SelectMany(e => e.Teams.Where(t => t.Value.Allocations.Count > 0).Select(t => new Guid(t.Key))).ToList(); // Get existing team allocations from DB var teamAllocations = DbContext.TeamAllocations.Where(x => x.ScenarioId.Equals(scenarioId) && teamsWithChangedAllocations.Contains(x.TeamId)) .GroupBy(x => x.ExpenditureCategoryId).ToDictionary(e => e.Key, eGrp => eGrp.GroupBy(x => x.TeamId).ToDictionary(t => t.Key, tGrp => tGrp.GroupBy(x => x.WeekEndingDate).ToDictionary(w => w.Key, x => x.FirstOrDefault()))); var teamAllocationToAddCollection = new List(); foreach (var expCatIdAsText in model.Expenditures.Keys) { var expCatId = new Guid(expCatIdAsText); var expCatModel = model.Expenditures[expCatIdAsText]; if (!teamAllocations.ContainsKey(expCatId)) teamAllocations.Add(expCatId, new Dictionary>()); foreach (var teamIdAsText in expCatModel.Teams.Keys) { var teamId = new Guid(teamIdAsText); var teamModel = expCatModel.Teams[teamIdAsText]; if (!teamAllocations[expCatId].ContainsKey(teamId)) teamAllocations[expCatId].Add(teamId, new Dictionary()); var currentTeamAllocations = teamAllocations[expCatId][teamId]; // Perform update or create for team allocations foreach (var weekendingAsText in teamModel.Allocations.Keys) { // Get allocations to update var weekending = Utils.ConvertFromUnixDate(Convert.ToInt64(weekendingAsText)); TeamAllocation allocationItem; decimal newValue = teamModel.Allocations[weekendingAsText] > 0 ? teamModel.Allocations[weekendingAsText] : 0; // Check resource allocation is out of the resource team membership dates if (!currentTeamAllocations.ContainsKey(weekending)) { // Create new allocation record, if it is correct allocationItem = new TeamAllocation { Id = Guid.NewGuid(), ScenarioId = scenarioId, ExpenditureCategoryId = expCatId, TeamId = teamId, WeekEndingDate = weekending, Quantity = newValue }; teamAllocationToAddCollection.Add(allocationItem); teamAllocations[expCatId][teamId].Add(weekending, allocationItem); // Track changes for audit tracker?.TeamAllocationValueAdded(allocationItem); } else { // Update existing allocation record allocationItem = currentTeamAllocations[weekending]; var oldValue = allocationItem.Quantity; allocationItem.Quantity = newValue; // Track changes for audit tracker?.TeamAllocationValueChanged(allocationItem, oldValue); } } } } DbContext.TeamAllocations.AddRange(teamAllocationToAddCollection); } private void UpdateScenarioAllocations(Scenario scenario, ActivityCalendarSaveModel.ScenarioSaveModel model, ScenarioRecalculationMode recalculationMode, IReadOnlyDictionary expenditureTypes, Dictionary> rates, IEnumerable weekEndings, ScenarioDetailsEventsTracker tracker = null) { if (recalculationMode == ScenarioRecalculationMode.None) // No need to recalculate allocations return; if (scenario == null) throw new ArgumentNullException(nameof(scenario)); if (weekEndings == null) throw new ArgumentNullException(nameof(weekEndings)); var recalculateEntireScenario = recalculationMode == ScenarioRecalculationMode.Complete; if (!scenario.StartDate.HasValue) throw new BLLException($"Scenario have no values for Start Date (id = {scenario.Id})"); // Get scenario details from DB var scenarioDetails = DbContext.ScenarioDetail .Where(x => (x.ParentID == scenario.Id) && (x.ExpenditureCategoryId.HasValue) && (x.WeekEndingDate.HasValue)) .GroupBy(x => x.ExpenditureCategoryId.Value).ToDictionary(e => e.Key, eGrp => eGrp.GroupBy(x => x.WeekEndingDate.Value).ToDictionary(w => w.Key, a => a.FirstOrDefault())); // Get scenario allocations and resource allocations from DB var teamAllocations = DbContext.TeamAllocations.Where(x => x.ScenarioId.Equals(scenario.Id)) .GroupBy(x => x.ExpenditureCategoryId).ToDictionary(e => e.Key, eGrp => eGrp.GroupBy(x => x.TeamId).ToDictionary(t => t.Key, tGrp => tGrp.GroupBy(x => x.WeekEndingDate).ToDictionary(w => w.Key, a => a.FirstOrDefault()))); var resourceAllocations = DbContext.PeopleResourceAllocations.AsNoTracking().Where(x => x.ScenarioId.Equals(scenario.Id)).ToList(); var requiredECs = recalculateEntireScenario ? scenarioDetails.Keys.Select(x => x.ToString()).ToList() : model.Expenditures.Keys.ToList(); var teamAllocationsToAdd = new List(); var scenarioDetailsToAdd = new List(); foreach (var expCatIdAsText in requiredECs) { var expCatId = new Guid(expCatIdAsText); var expCatModel = model.Expenditures.ContainsKey(expCatIdAsText) ? model.Expenditures[expCatIdAsText] : null; var isSuperExpCat = expenditureTypes.ContainsKey(expCatId) && !expenditureTypes[expCatId]; var hasChangedAllocations = expCatModel != null && expCatModel.Teams.Values.Any(t => ((t.Allocations.Count > 0) || t.Resources.Values.Any(r => (r.ForecastAllocations.Count > 0) || r.IsRemoved))); if ((recalculationMode == ScenarioRecalculationMode.SuperExpendituresOnly) && !isSuperExpCat) continue; if ((recalculationMode == ScenarioRecalculationMode.SuperExpendituresAndChangedAllocations) && !isSuperExpCat && !hasChangedAllocations) continue; var detailsWeekendingsToUpdate = new List(); var requiredTeams = new List(); if (recalculateEntireScenario && teamAllocations.ContainsKey(expCatId)) requiredTeams.AddRange(teamAllocations[expCatId].Keys.Select(x => x.ToString())); if (expCatModel != null && expCatModel.Teams != null) requiredTeams.AddRange(expCatModel.Teams.Keys.ToList()); foreach (var teamIdAsText in requiredTeams.Distinct()) { var teamId = new Guid(teamIdAsText); var teamModel = expCatModel != null && expCatModel.Teams.ContainsKey(teamIdAsText) ? expCatModel.Teams[teamIdAsText] : null; Dictionary teamWeekendingsToUpdate = null; #region Recalculation of team allocations if (recalculateEntireScenario || (teamModel != null && teamModel.Resources.Values.Any(x => x.IsRemoved))) { // Allocations should be recalculated for all team weekendings teamWeekendingsToUpdate = weekEndings.ToDictionary(k => k, v => Convert.ToString(Utils.ConvertToUnixDate(v))); } else { // Allocations should be recalculated for a list of weekendings, scenario has changes on teamWeekendingsToUpdate = teamModel?.Resources.Values.SelectMany(x => x.ForecastAllocations.Keys) .Union(teamModel.Allocations.Keys) // Add weekending for changed team allocations .Distinct().ToDictionary(k => Utils.ConvertFromUnixDate(Convert.ToInt64(k)), v => v) ?? new Dictionary(); } foreach (var we in teamWeekendingsToUpdate.Keys) { TeamAllocation allocationItem = null; string weAsText = teamWeekendingsToUpdate[we]; var allocationItemWasCreated = false; if (teamAllocations.ContainsKey(expCatId) && teamAllocations[expCatId].ContainsKey(teamId) && teamAllocations[expCatId][teamId].ContainsKey(we)) { // Allocation records exists in DB allocationItem = teamAllocations[expCatId][teamId][we]; } else { allocationItem = new TeamAllocation() { Id = Guid.NewGuid(), ScenarioId = scenario.Id, ExpenditureCategoryId = expCatId, TeamId = teamId, WeekEndingDate = we, Quantity = 0 }; teamAllocationsToAdd.Add(allocationItem); allocationItemWasCreated = true; // Add created team allocation records to indexed struct for use in details recalculations if (!teamAllocations.ContainsKey(expCatId)) teamAllocations.Add(expCatId, new Dictionary>()); if (!teamAllocations[expCatId].ContainsKey(teamId)) teamAllocations[expCatId].Add(teamId, new Dictionary()); if (!teamAllocations[expCatId][teamId].ContainsKey(we)) teamAllocations[expCatId][teamId].Add(we, allocationItem); } // Perform agregation from resource level to team level var foundResourceRecords = resourceAllocations.Where(x => x.ExpenditureCategoryId.Equals(expCatId) && x.TeamId.Equals(teamId) && (x.WeekEndingDate == we)) .ToList(); decimal newValue = 0; if (foundResourceRecords.Count > 0) // Scenario Team contains resources assigned. Get Team allocation value as summ of the assigned resource allocations newValue = foundResourceRecords.Sum(x => x.Quantity); else { if (teamModel != null && teamModel.Allocations.ContainsKey(weAsText)) // Team allocations were changed. As this team has no resources assigned, get incoming model value newValue = teamModel.Allocations[weAsText]; } var oldValue = allocationItem.Quantity; allocationItem.Quantity = newValue > 0 ? newValue : 0; if (tracker != null) { // Track changes for audit if (allocationItemWasCreated) tracker.TeamAllocationValueAdded(allocationItem, true); else tracker.TeamAllocationValueChanged(allocationItem, oldValue, true); } } detailsWeekendingsToUpdate.AddRange(teamWeekendingsToUpdate.Keys); #endregion } #region Recalculation of Scenario Details detailsWeekendingsToUpdate = detailsWeekendingsToUpdate.Distinct().OrderBy(x => x).ToList(); foreach (var we in detailsWeekendingsToUpdate) { ScenarioDetail detailItem; var detailsRecordWasCreated = false; if (scenarioDetails.ContainsKey(expCatId) && scenarioDetails[expCatId].ContainsKey(we)) { // Details records exists in DB detailItem = scenarioDetails[expCatId][we]; } else { detailItem = new ScenarioDetail { Id = Guid.NewGuid(), ParentID = scenario.Id, ExpenditureCategoryId = expCatId, WeekEndingDate = we, WeekOrdinal = 0, Quantity = 0, Cost = 0 }; scenarioDetailsToAdd.Add(detailItem); detailsRecordWasCreated = true; } // Perform agregation from team level to EC level var foundTeamAllocationRecords = new List(); if (teamAllocations.ContainsKey(expCatId)) { foundTeamAllocationRecords = teamAllocations[expCatId].Values .SelectMany(t => t.Values.Where(a => (a.WeekEndingDate == we))).ToList(); } decimal newQuantityValue = (foundTeamAllocationRecords.Count > 0) ? foundTeamAllocationRecords.Sum(x => x.Quantity) : 0; if (newQuantityValue < 0) newQuantityValue = 0; decimal newCostValue = newQuantityValue * RateManager.GetRateValue(rates, expCatId, we); var oldQuantityValue = detailItem.Quantity; var oldCostValue = detailItem.Cost; detailItem.Quantity = newQuantityValue; detailItem.Cost = newCostValue; if (tracker != null) { // Track changes for audit if (detailsRecordWasCreated) tracker.ScenarioDetailsValueAdded(detailItem, true); else tracker.ScenarioDetailsValueChanged(detailItem, oldQuantityValue, oldCostValue, true); } } #endregion } DbContext.TeamAllocations.AddRange(teamAllocationsToAdd); DbContext.ScenarioDetail.AddRange(scenarioDetailsToAdd); DbContext.BulkSaveChanges(); } private bool AreScenarioActualsChanged(ActivityCalendarSaveModel.ScenarioSaveModel model) { if (model == null) throw new ArgumentNullException("model"); bool result = model.Expenditures.Values .Any(e => e.Teams.Values.Any(t => t.Resources.Values.Any(r => (r.ActualAllocations.Count > 0) || r.IsRemoved))); return result; } private bool AreScenarioProjectionsChanged(ActivityCalendarSaveModel.ScenarioSaveModel model) { if (model == null) throw new ArgumentNullException("model"); bool result = model.Expenditures.Values .Any(e => e.Teams.Values.Any(t => t.Resources.Values.Any(r => (r.ForecastAllocations.Count > 0) || r.IsRemoved))); return result; } private bool AreScenarioTeamAllocationsChanged(ActivityCalendarSaveModel.ScenarioSaveModel model) { if (model == null) throw new ArgumentNullException("model"); bool result = model.Expenditures.Values .Any(e => e.Teams.Values.Any(t => t.Allocations.Count > 0)); return result; } private List GetScenarioNewTeams(Guid parentProjectId, ActivityCalendarSaveModel.ScenarioSaveModel model) { if (model == null) throw new ArgumentNullException("model"); var changedTeams = model.Expenditures.Values.SelectMany(x => x.Teams.Select(t => t.Key)) .Select(x => new Guid(x)).ToList(); var projectTeams = DbContext.Team2Project.Where(x => x.ProjectId == parentProjectId).Select(x => x.TeamId).ToList(); var newTeams = changedTeams.Except(projectTeams).ToList(); return newTeams; } private List GetScenarioChangedTeams(ActivityCalendarSaveModel.ScenarioSaveModel model) { if (model == null) throw new ArgumentNullException("model"); var changedTeams = model.Expenditures.Values.SelectMany(x => x.Teams.Keys).Select(x => new Guid(x)).ToList(); return changedTeams; } /// /// Returns D[scenarioId, bool]. True for scenario, if it contains at least one SuperExpenditure, otherwise, False /// private Dictionary ScenariosContainsSuperExpenditures(List scenarios) { if (scenarios == null) throw new ArgumentNullException(nameof(scenarios)); if (scenarios.Count < 1) return new Dictionary(); var scenariosHasSuperExpCats = DbContext.ScenarioDetail.AsNoTracking().Include(x => x.ExpenditureCategory) .Where(x => x.ParentID.HasValue && scenarios.Contains(x.ParentID.Value) && !x.ExpenditureCategory.AllowResourceAssignment) .GroupBy(x => x.ParentID.Value).ToDictionary(k => k.Key, grp => grp.Any()); return scenariosHasSuperExpCats; } private List GetScenarioActualsChangedWeekendings(ActivityCalendarSaveModel.ScenarioSaveModel scenarioModel) { if (scenarioModel == null) throw new ArgumentNullException("model"); var result = scenarioModel.Expenditures.Values .SelectMany(e => e.Teams.Values.SelectMany(t => t.Resources.Values.SelectMany(r => r.ActualAllocations.Keys))) .Distinct().Select(x => Convert.ToInt64(x)).ToList(); return result; } private List GetScenarioActualsChangedExpenditures(ActivityCalendarSaveModel.ScenarioSaveModel scenarioModel) { if (scenarioModel == null) throw new ArgumentNullException("model"); var result = scenarioModel.Expenditures.Keys.Where(expCatId => scenarioModel.Expenditures[expCatId].Teams.Values.Any(t => t.Resources.Values.Any(r => r.ActualAllocations.Count > 0))) .Select(x => new Guid(x)).ToList(); return result; } private ScenarioRecalculationMode GetScenarioRecalculationMode(ActivityCalendarSaveModel model, Guid scenarioId, bool scenarioIsBottomUp, bool scenarioHasProjectionsToUpdate, bool scenarioHasTeamAllocationsToUpdate) { if (model == null) throw new ArgumentNullException("model"); if (scenarioId.Equals(Guid.Empty)) throw new ArgumentNullException("scenarioId"); var scenarioIdAsText = scenarioId.ToString(); if (!model.Scenarios.ContainsKey(scenarioIdAsText)) throw new BLLException(String.Format("Scenario not found in data save model (Id = {0})", scenarioIdAsText)); var scenarioModel = model.Scenarios[scenarioIdAsText]; // Determine scenario recalculation mode var scenarioUpdateMode = ScenarioRecalculationMode.None; if ((scenarioHasProjectionsToUpdate || scenarioHasTeamAllocationsToUpdate) && scenarioModel.UpdateProjections) scenarioUpdateMode = ScenarioRecalculationMode.SuperExpendituresAndChangedAllocations; if (scenarioIsBottomUp) scenarioUpdateMode = ScenarioRecalculationMode.Complete; return scenarioUpdateMode; } private void FilterTeamsByResources(List teams, List resourcesToKeep) { if (teams == null) throw new ArgumentNullException("teams"); if ((resourcesToKeep == null) || (resourcesToKeep.Count < 1)) return; var keepIdsAsText = resourcesToKeep.Select(x => x.ToString()).ToList(); for (var tIndex = teams.Count - 1; tIndex >= 0; tIndex--) { var team = teams[tIndex]; if (team.ExpCategories != null) { var expCatKeys = team.ExpCategories.Keys.ToList(); foreach (var expCatId in expCatKeys) { var expCat = team.ExpCategories[expCatId]; if (expCat.Resources != null) { var resourceKeysToRemove = expCat.Resources.Keys.ToList().Except(keepIdsAsText).ToList(); foreach (var resourceKey in resourceKeysToRemove) { expCat.Resources.Remove(resourceKey); } } if ((expCat.Resources == null) || (expCat.Resources.Count < 1)) { team.ExpCategories.Remove(expCatId); } } } if ((team.ExpCategories == null) || (team.ExpCategories.Count < 1)) teams.RemoveAt(tIndex); } } private void SetNonProjectTimeEditPermissions(List teams, Dictionary teamsAccess) { if ((teams == null) || (teamsAccess == null) || (teams.Count < 1) || (teamsAccess.Count < 1)) return; var teamIds = teamsAccess.Keys.ToList(); var prMngr = new PeopleResourcesManager(DbContext); var nptMngr = new NonProjectTimeManager(DbContext); // Get list of team-wide NPTs in teams collection and extract team list for every NPT-item. // Compare team list for every NPT-item with incoming teamIds to find out the NPT-item is // editable or not. NPT-item seems to be editable if it's team list equals to incoming teamIds list var teamWideNpts = teams.Where(t => (t.ExpCategories != null)).SelectMany(t => t.ExpCategories.Values) .Where(e => (e.Resources != null)).SelectMany(e => e.Resources.Values) .Where(r => (r.NonProjectTime != null)).SelectMany(r => r.NonProjectTime.Values) .Where(n => (n != null) && n.isTeamWide).Select(n => n.Id).Distinct().ToList(); var teamWideNptItems = nptMngr.GetNonProjectTimes(teamWideNpts, true); var teamWideNptsEditableStatus = teamWideNptItems.Values.Select(x => new { Id = x.Id, IsEditable = (x.Teams != null) && (x.Teams.Count > 0) && (x.Teams.Except(teamIds).Count() < 1) && x.Teams.TrueForAll(t => teamsAccess.ContainsKey(t) && (teamsAccess[t] == AccessLevel.Write)) }).GroupBy(x => x.Id).ToDictionary(grp => grp.Key, v => v.Select(z => z.IsEditable).Min()); // Get access permissions for resources in teams and set resource-level NPT editable, if // the corresponding resource is editable for this team var resourcesWithTeams = prMngr.GetPeopleResourceWithTeams4Teams(teamIds); var resourcesInTeamsEditableStatus = resourcesWithTeams.Where(x => (x.Teams != null)).ToDictionary(x => x.Id, x => x.Teams.Select(t => new { Id = t.TeamId, IsEditable = !t.ReadOnly }).GroupBy(t => t.Id).ToDictionary(k => k.Key, grp => grp.Select(r => r.IsEditable).FirstOrDefault())); foreach (var team in teams) { if (team.ExpCategories != null) { var teamId = new Guid(team.Id); foreach (var expCat in team.ExpCategories.Values) { if (expCat.Resources != null) { foreach (var resource in expCat.Resources.Values) { if (resource.NonProjectTime != null) { foreach (string nptKey in resource.NonProjectTime.Keys) { var nptItem = resource.NonProjectTime[nptKey]; if (nptItem.isTeamWide) { nptItem.isReadOnly = teamWideNptsEditableStatus.ContainsKey(nptItem.Id) ? !teamWideNptsEditableStatus[nptItem.Id] : true; } else { nptItem.isReadOnly = resourcesInTeamsEditableStatus.ContainsKey(resource.Id) && resourcesInTeamsEditableStatus[resource.Id].ContainsKey(teamId) ? !resourcesInTeamsEditableStatus[resource.Id][teamId] : true; } } } } } } } } } private IEnumerable GetCostCenters(IEnumerable needAllocations, IEnumerable teamAllocations, IEnumerable projectRolesInfo, bool ignoreExpendituresWithoutTeamAllocations) { // Gather all expenditures in the model and get cost centers var costCenterMngr = (new CreditDepartmentManager(DbContext)); var expCatsWithNeed = (needAllocations != null) ? needAllocations.Where(x => x.ExpenditureCategoryId.HasValue && x.WeekEndingDate.HasValue && x.Quantity.HasValue && (x.Quantity.Value > 0)) .Select(x => x.ExpenditureCategoryId.Value).Distinct().ToList() : null; if (expCatsWithNeed != null) { // Check team allocations existance only for ordinary ECs var projectRolesInfoIds = (projectRolesInfo != null) ? projectRolesInfo.Select(x => x.ExpenditureCategoryId).ToList() : new List(); var teamAllocationsFiltered = (teamAllocations != null) ? teamAllocations.Where(x => expCatsWithNeed.Contains(x.ExpenditureCategoryId)).ToList() : new List(); expCatsWithNeed = expCatsWithNeed.Where(e => projectRolesInfoIds.Contains(e) || teamAllocationsFiltered.Any(ta => (ta.ExpenditureCategoryId == e) && (ta.Quantity > 0))).ToList(); } var result = costCenterMngr.GetCreditDepartmentsByExpCats(expCatsWithNeed); return result; } private IEnumerable GetProjectRoles(IEnumerable needAllocations, IEnumerable teamAllocations, IEnumerable costCenters, IEnumerable projectRolesInfo) { if ((needAllocations == null) || (projectRolesInfo == null)) return null; // Get all Project Role from data model, for which we have extended info var infoForProjectRoleIds = projectRolesInfo.Select(x => x.ExpenditureCategoryId).ToList(); // Get project roles with non-zero Need values - by scenarios var projectRolesNeedIndexed = needAllocations.Where(x => x.ParentID.HasValue && x.ExpenditureCategoryId.HasValue && x.WeekEndingDate.HasValue && infoForProjectRoleIds.Contains(x.ExpenditureCategoryId.Value)) .GroupBy(x => x.ParentID.Value).ToDictionary(g0 => g0.Key, g0 => g0.GroupBy(x => x.ExpenditureCategoryId.Value).ToDictionary(g1 => g1.Key, g1 => g1.ToDictionary(k2 => k2.WeekEndingDate.Value, v2 => v2.Quantity.HasValue ? v2.Quantity.Value : 0))); // Get project roles with non-zero need and format team allocations for quick access - by scenarios var projectRolesWithNonZeroNeed = projectRolesNeedIndexed.SelectMany(x => x.Value.Where(z => z.Value.Values.Any(v => v > 0)).Select(z => z.Key)).Distinct().ToList(); var teamAllocationsForRolesIndexed = teamAllocations.Where(x => projectRolesWithNonZeroNeed.Contains(x.ExpenditureCategoryId)) .GroupBy(x => x.ScenarioId).ToDictionary(g0 => g0.Key, g0 => g0.GroupBy(x => x.ExpenditureCategoryId).ToDictionary(g1 => g1.Key, g1 => g1.GroupBy(y => y.WeekEndingDate).ToDictionary(g2 => g2.Key, g2 => g2.Select(z => z.Quantity).Sum()))); // Get Project roles with non-zero remaining need (need != Sum(Team allocations)). Look for roles in every scenario var rolesWithNonZeroRemainingNeedDuplicated = new List(); foreach (var scenarioId in projectRolesNeedIndexed.Keys) { var scenarioNeedData = projectRolesNeedIndexed[scenarioId]; var projectRolesToLook = scenarioNeedData.Keys.Intersect(projectRolesWithNonZeroNeed).ToList(); if (!teamAllocationsForRolesIndexed.ContainsKey(scenarioId)) { rolesWithNonZeroRemainingNeedDuplicated.AddRange(projectRolesToLook); } else { var scenarioTeamAllocationsData = teamAllocationsForRolesIndexed[scenarioId]; foreach (var projectRoleId in projectRolesToLook) { var hasNonZeroRemainingNeed = false; var currentRoleNeed = scenarioNeedData[projectRoleId]; if (scenarioTeamAllocationsData.ContainsKey(projectRoleId)) { // Some teams are allocated to current project role var allocationsToCurrentRole = scenarioTeamAllocationsData[projectRoleId]; var weekendings = currentRoleNeed.Keys; foreach (var we in weekendings) { var currentWeekendingNeed = currentRoleNeed[we]; var currentWeekendingAllocations = allocationsToCurrentRole.ContainsKey(we) ? allocationsToCurrentRole[we] : 0; if (currentWeekendingNeed > currentWeekendingAllocations) { hasNonZeroRemainingNeed = true; break; } } } else { // No teams allocated to project role. But project role has non-zero need hasNonZeroRemainingNeed = true; } if (hasNonZeroRemainingNeed) { rolesWithNonZeroRemainingNeedDuplicated.Add(projectRoleId); } } } } var rolesWithNonZeroRemainingNeed = rolesWithNonZeroRemainingNeedDuplicated.Distinct().ToList(); var costCentersIds = (costCenters != null) ? costCenters.Select(x => x.Id).ToList() : new List(); var result = projectRolesInfo.Where(x => rolesWithNonZeroRemainingNeed.Contains(x.ExpenditureCategoryId) && ((costCentersIds.Count < 1) || costCentersIds.Contains(x.CreditId))).ToList(); return result; } private List ConvertCostCentersToOptions(IEnumerable costCenters) { if (costCenters == null) return null; var result = costCenters.Select(x => new SelectListItem() { Value = x.Id.ToString(), Text = x.Name }).OrderBy(x => x.Text).ToList(); return result; } private List ConvertExpendituresToOptions(IEnumerable expCats) { if (expCats == null) return null; var result = expCats.Select(x => new ActivityCalendarFilterOptions.ProjectRoleItem() { Value = x.ExpenditureCategoryId.ToString(), Text = x.ExpenditureCategoryName, CostCenterId = x.CreditId.ToString() }).OrderBy(x => x.Text).ToList(); return result; } #endregion #region Overrides public void Dispose() { if (_isContexLocal) _dbContext.Dispose(); } #endregion } }