using System.Globalization; using System.Web.Mvc; using EnVisage.Code.DAL; using EnVisage.Code.DAL.Mongo; using EnVisage.Models; using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Driver; using NLog; using System.Threading.Tasks; namespace EnVisage.Code.BLL { public class MongoMixManager : IMixManager { protected Logger _logger = NLog.LogManager.GetCurrentClassLogger(); private string _userId = string.Empty; private readonly EnVisageEntities _dbContext; protected EnVisageEntities DbContext { get { return _dbContext; } } private bool _isContexLocal; public MongoMixManager(EnVisageEntities dbContext, string userId) { if (dbContext == null) { _dbContext = new EnVisageEntities(); _isContexLocal = true; } else { _dbContext = dbContext; } _userId = userId; } protected DAL.Mongo.Mix InitInstance() { return new DAL.Mongo.Mix(); } protected DAL.Mongo.Mix RetrieveReadOnlyById(string key) { return MongoDataContext.Mixes.Find(t => t.Id == new ObjectId(key)).FirstOrDefaultAsync().GetAwaiter().GetResult(); } public void Delete(string mixId) { if (!string.IsNullOrWhiteSpace(mixId)) { var objectId = ObjectId.Empty; if (ObjectId.TryParse(mixId, out objectId) && !ObjectId.Empty.Equals(objectId)) { Delete(objectId); } } } public void Delete(ObjectId mixId) { if (!ObjectId.Empty.Equals(mixId)) { var result = MongoDataContext.Mixes.DeleteOneAsync(t => t.Id == mixId).GetAwaiter().GetResult(); if (!result.IsAcknowledged) _logger.Warn("An error occurred while removing Mix record"); } var allocResult = MongoDataContext.ExpenditureAllocations.DeleteManyAsync(t => t.MixId == mixId).GetAwaiter().GetResult(); if (!allocResult.IsAcknowledged) _logger.Warn("An error occurred while removing Mix expenditure allocations"); allocResult = MongoDataContext.TeamAllocations.DeleteManyAsync(t => t.MixId == mixId).GetAwaiter().GetResult(); if (!allocResult.IsAcknowledged) _logger.Warn("An error occurred while removing Mix team allocations"); allocResult = MongoDataContext.ResourceAllocations.DeleteManyAsync(t => t.MixId == mixId).GetAwaiter().GetResult(); if (!allocResult.IsAcknowledged) _logger.Warn("An error occurred while removing Mix resource allocations"); } private Mix Save(Mix dbObj) { dbObj.CreatedAtUtc = DateTime.UtcNow; dbObj.CreatedBy = _userId; MongoDataContext.Mixes.InsertOneAsync(dbObj).GetAwaiter().GetResult(); return dbObj; } public Mix SaveWithChildren(MixSaveModel model) { #region step 0. Load old mix object var savedMix = RetrieveWithChildren(model.Id); #endregion #region step 1. Save Mix object Mix dbObj = InitInstance(); model.CopyTo(dbObj); dbObj = Save(dbObj); #endregion #region step 2. Save Allocations try { var expAllocations2Insert = new List(); var teamAllocations2Insert = new List(); var resAllocations2Insert = new List(); // get all scenarios in mix var scenariosInMix = model.Calendar.Projects.Where(x => x.Value != null && x.Value.Scenario != null) .Select(x => x.Value.Scenario.Id).ToList(); // check which scenarios from mix still exist in the live database var existenceScenarios = DbContext.Scenarios.Where(x => scenariosInMix.Contains(x.Id)).Select(x => x.Id).ToList(); foreach (var projectId in model.Calendar.Projects.Keys) { var project = model.Calendar.Projects[projectId]; if (project.Scenario == null) continue; var currentScenario = project.Scenario; #region Determining expenditures for saving var expenditures = currentScenario.Expenditures; // expenditures can be empty if user didn't edit this project befor saving this mix in current moment if (expenditures == null || expenditures.Count <= 0) { // if mix has been saved earlier and it contains expenditures we should to save them for the current mix if (savedMix != null && savedMix.Calendar != null && savedMix.Calendar.Projects != null) { if (savedMix.Calendar.Projects.ContainsKey(projectId)) { var savedProject = savedMix.Calendar.Projects[projectId]; if (savedProject.Scenario != null) expenditures = savedProject.Scenario.Expenditures; } } } if (expenditures == null || expenditures.Count <= 0) { // TODO: review for ability to change GetFullAllocationInfoByScenario method for retrieving information for a couple of scenarios if (existenceScenarios.Contains(currentScenario.Id)) expenditures = (new ScenarioManager(DbContext)).GetFullAllocationInfoByScenario(currentScenario.Id, _userId); } #endregion if (expenditures == null || expenditures.Count <= 0) continue; foreach (var expCat in expenditures) { // insert expenditure allocations var expenditureAllocation = new MixExpenditureAllocation { MixId = dbObj.Id, ScenarioId = currentScenario.Id, CreatedAtUtc = DateTime.UtcNow, CreatedBy = _userId }; expCat.Value.CopyTo(expenditureAllocation); expAllocations2Insert.Add(expenditureAllocation); // insert team allocations foreach (var expenditureDetailsTeam in expCat.Value.Teams) { var teamAllocation = new MixTeamAllocation() { MixId = dbObj.Id, ScenarioId = currentScenario.Id, ExpCatId = expenditureAllocation.ExpCatId, CreatedAtUtc = DateTime.UtcNow, CreatedBy = _userId, }; expenditureDetailsTeam.Value.CopyTo(teamAllocation); teamAllocations2Insert.Add(teamAllocation); foreach (var teamResource in expenditureDetailsTeam.Value.Resources) { var resAllocation = new MixResourceAllocation { MixId = dbObj.Id, ScenarioId = currentScenario.Id, ExpCatId = expenditureAllocation.ExpCatId, TeamId = teamAllocation.TeamId, CreatedAtUtc = DateTime.UtcNow, CreatedBy = _userId, }; teamResource.Value.CopyTo(resAllocation); resAllocations2Insert.Add(resAllocation); } } } } SaveAllocations(expAllocations2Insert, teamAllocations2Insert, resAllocations2Insert); } catch (Exception exception) { _logger.Fatal(exception); // if any error occurred while saving new Mix then remove it and all it's children. Delete(dbObj.Id); // Then throw exception to ask user to try again later throw; } #endregion #region step 3. Update QuickLinks try { // try to update quick links var query = DbContext.UserQuickLinks.Where(t => t.Url.Contains(model.Id)); foreach (var item in query) { item.Url = item.Url.Replace(model.Id, dbObj.Id.ToString()); DbContext.Entry(item).State = EntityState.Modified; } } catch (Exception exception) { // if any error occurred while updating quick links then do nothing except error logging _logger.Fatal(exception); } #endregion #region step 4. Delete previous Mix with children try { // try to delete previous Mix record Delete(model.Id); } catch (Exception exception) { // if any error occurred while removing old Mix then do nothing except error logging _logger.Fatal(exception); } #endregion return dbObj; } private void SaveAllocations(List expenditures, List teamAllocations, List resourceAllocations) { var tasks = new List(); if (expenditures != null && expenditures.Count > 0) tasks.Add(MongoDataContext.ExpenditureAllocations.InsertManyAsync(expenditures)); if (teamAllocations != null && teamAllocations.Count > 0) tasks.Add(MongoDataContext.TeamAllocations.InsertManyAsync(teamAllocations)); if (resourceAllocations != null && resourceAllocations.Count > 0) tasks.Add(MongoDataContext.ResourceAllocations.InsertManyAsync(resourceAllocations)); Task.WaitAll(tasks.ToArray()); } public DAL.Mongo.Mix Retrieve(string mixId) { if (string.IsNullOrWhiteSpace(mixId)) return null; return MongoDataContext.Mixes.Find(t => t.Id == new ObjectId(mixId)).FirstOrDefaultAsync().GetAwaiter().GetResult(); } public MixSaveModel RetrieveModel(string mixId) { if (string.IsNullOrWhiteSpace(mixId)) return null; var mix = Retrieve(mixId); if (null == mix) return null; MixSaveModel model = new MixSaveModel(); model.Id = mix.Id.ToString(); model.Name = mix.Name; model.StartDate = mix.StartDate; model.EndDate = mix.EndDate; model.Filter.Selection.StartDate = mix.StartDate; model.Filter.Selection.EndDate = mix.EndDate; model.Filter.Selection.TeamsViews = mix.TeamsViews.Select(x => new MixTeamViewModel() { Id = x.Id, TVName = x.Name, Group = new SelectListGroup() { Disabled = false, Name = x.Group }, IsNew = x.IsNew, CapacityTeamId = x.CapacityTeamId, CompanyId = x.CompanyId, CopyPlanned = x.CopyPlanned, CostCenterId = x.CostCenterId, Data = x.Data, UserId = x.UserId }).ToList(); model.Users = mix.Users; model.Calendar.AssignFrom(mix); return model; } public MixSaveModel RetrieveWithChildren(string mixId) { var model = RetrieveModel(mixId); if (model == null) return null; #region Load Allocations var scenarios = model.Calendar.Projects.Where(x => x.Value != null && x.Value.Scenario != null) .Select(x => x.Value.Scenario.Id) .ToList(); var allocations = GetAllocations4Scenarios(mixId, scenarios); if (allocations != null && allocations.Count > 0) { foreach (var projectId in model.Calendar.Projects.Keys) { var project = model.Calendar.Projects[projectId]; if (project.Scenario == null || !allocations.ContainsKey(project.Scenario.Id)) continue; project.Scenario.Expenditures = allocations[project.Scenario.Id]; } } #endregion return model; } public List GetProjectModel(List projects) { var scenarioManager = (new ScenarioManager(DbContext)); var result = new List(); var availableProjects = DbContext.Projects.Where(x => projects.Contains(x.Id)) .Include(x => x.Team2Project) .Include(x => x.ParentProject) .Include(x => x.Scenarios) .Include(x => x.Scenarios.Select(s => s.CostSavings1)) .ToList(); string defaultColor = ""; var settings = DbContext.SystemSettings.Where( item => item.Type == (int)SystemSettingType.DefaultProjectColorType).FirstOrDefault(); if (settings != null) { defaultColor = settings.Value; } var projectsModel = availableProjects.Select(x => new { Id = x.Id, Name = x.ParentProjectId.HasValue ? x.Name + ": " + x.ParentProject.Name : x.Name, Color = (x.ParentProjectId.HasValue && (x.Color == null || x.Color.Trim() == "")) ? ((x.ParentProject.Color == null || x.ParentProject.Color.Trim() == "") ? defaultColor : x.ParentProject.Color) : ((x.Color == null || x.Color.Trim() == "") ? defaultColor : x.Color), Deadline = x.Deadline, Teams = x.Team2Project.Select(ts => ts.TeamId).Distinct().ToList(), ActiveScenario = x.Scenarios.FirstOrDefault(s => s.StartDate.HasValue && s.EndDate.HasValue && s.Type == (int)ScenarioType.Portfolio && s.Status == (int)ScenarioStatus.Active), InactiveScenarios = x.Scenarios.Where(s => s.StartDate.HasValue && s.EndDate.HasValue && s.Type == (int)ScenarioType.Portfolio && s.Status == (int)ScenarioStatus.Inactive).ToList() }).ToList(); var activeScenariosIds = projectsModel.Where(x => x.ActiveScenario != null).Select(x => x.ActiveScenario.Id).ToList(); var inactiveScenariosIds = projectsModel.Where(x => x.InactiveScenarios != null).SelectMany(x => x.InactiveScenarios.Select(s => s.Id).ToList()).ToList(); var scenarios = activeScenariosIds.Union(inactiveScenariosIds).ToList(); var rates = (new RateManager(_dbContext)).Get4Parents(scenarios, RateModel.RateType.Derived); foreach (var project in projectsModel) { var strColor = !String.IsNullOrEmpty(project.Color) ? string.Format("#{0}", project.Color) : String.Empty; var color = System.Drawing.ColorTranslator.FromHtml(strColor); var activeScenario = (ScenarioCalendarMixModel)project.ActiveScenario; var inactiveScenarios = project.InactiveScenarios.OrderBy(z => z.Name).ToDictionary(x => x.Id.ToString(), x => (ScenarioCalendarMixModel)x); if (activeScenario != null && rates.ContainsKey(activeScenario.Id)) { var expRates = rates[activeScenario.Id].GroupBy(x => x.ExpenditureCategoryId).ToDictionary(x => x.Key, g => g.ToList()); activeScenario.Rates = scenarioManager.GetRatesModel(expRates); } if (inactiveScenarios != null && inactiveScenarios.Count > 0) { foreach (var scenario in inactiveScenarios) { if (rates.ContainsKey(scenario.Value.Id)) { var expRates = rates[scenario.Value.Id].GroupBy(x => x.ExpenditureCategoryId).ToDictionary(x => x.Key, g => g.ToList()); scenario.Value.Rates = scenarioManager.GetRatesModel(expRates); } } } result.Add(new MixProjectModel() { Id = project.Id, Name = project.Name, Color = strColor, ColorRGB = !color.IsEmpty ? string.Format("{0}, {1}, {2}", color.R, color.G, color.B) : String.Empty, Deadline = project.Deadline.HasValue ? Utils.ConvertToUnixDate(project.Deadline.Value) : (long?)null, Teams = project.Teams, Scenario = activeScenario, InactiveScenarios = inactiveScenarios }); } return result; } public void ActivateMix(string mixId) { if (string.IsNullOrWhiteSpace(mixId)) throw new ArgumentNullException("mixId"); var mix = RetrieveWithChildren(mixId); if (mix == null) throw new InvalidOperationException(string.Format("Mix with Id [{0}] can not be activated because it does not exists.", mixId)); if (mix.Calendar == null) return; if (mix.Calendar.Teams != null && mix.Calendar.Teams.Any(x => x.IsNew)) { var teamManager = (new TeamManager(DbContext)); var rateManager = (new RateManager(DbContext)); var newTeams = mix.Calendar.Teams.FindAll(x => x.IsNew); var expCatsInTeams = newTeams.Where(x => x.PlannedCapacity != null).SelectMany(x => x.PlannedCapacity.Select(t => t.ExpCatId)).ToList(); var expCatsInTeamsGuid = new List(); foreach (var expCatString in expCatsInTeams) { Guid expCatId; if (string.IsNullOrWhiteSpace(expCatString) || !Guid.TryParse(expCatString, out expCatId) || expCatId == Guid.Empty) continue; expCatsInTeamsGuid.Add(expCatId); } var rates = rateManager.GetRates(expCatsInTeamsGuid, RateModel.RateType.Global); Guid currentUser = (_userId.Length > 0) ? new Guid(_userId) : Guid.Empty; // SA. ENV-1083 foreach (var team in newTeams) { // SA. ENV-1083. Automatically add current user to team contributors. if (!currentUser.Equals(Guid.Empty)) { if (team.UserId == null) team.UserId = new Guid[1] { currentUser }; else if (!team.UserId.Contains(currentUser)) { List tmpUsersList = team.UserId.ToList(); tmpUsersList.Add(currentUser); team.UserId = tmpUsersList.ToArray(); } } var savedTeam = teamManager.Save((TeamModel)team, true); if (savedTeam == null || !savedTeam.PlannedCapacityScenarioId.HasValue) continue; if (team.PlannedCapacity == null || team.PlannedCapacity.Count <= 0) continue; foreach (var capacity in team.PlannedCapacity) { if (capacity.Values == null || capacity.Values.Count <= 0) continue; Guid expCatId; if (string.IsNullOrWhiteSpace(capacity.ExpCatId) || !Guid.TryParse(capacity.ExpCatId, out expCatId) || expCatId == Guid.Empty) continue; foreach (var value in capacity.Values) { long weekEnding; if (!long.TryParse(value.Key, out weekEnding) || weekEnding <= 0) continue; DbContext.ScenarioDetail.Add(new ScenarioDetail() { Id = Guid.NewGuid(), ParentID = savedTeam.PlannedCapacityScenarioId.Value, ExpenditureCategoryId = expCatId, WeekEndingDate = Utils.ConvertFromUnixDate(weekEnding), Quantity = value.Value, Cost = value.Value * rateManager.GetRateValue(rates, expCatId, Utils.ConvertFromUnixDate(weekEnding)), LastUpdate = DateTime.UtcNow }); } } } // we should to save all new teams in one separate transaction because every scenario will be saved in the own transaction // and it need actual information abuout teams DbContext.SaveChanges(); } if (mix.Calendar.Projects != null && mix.Calendar.Projects.Count > 0) { // SA. ENV-1159. Get list of projects for activation List projectsToActivate = new List(); if ((mix.Calendar.ManagedProjects != null) && (mix.Calendar.ManagedProjects.Count > 0)) // All managed projects projectsToActivate.AddRange(mix.Calendar.ManagedProjects); if ((mix.Calendar.QueuedProjects != null) && (mix.Calendar.QueuedProjects.Count > 0)) { // Include queued projects, that have newly created scenarios in the Mix projectsToActivate.AddRange(mix.Calendar.QueuedProjects.Where(x => mix.Calendar.Projects.ContainsKey(x.ToString()) && (mix.Calendar.Projects[x.ToString()].Scenario != null) && mix.Calendar.Projects[x.ToString()].Scenario.IsNew)); } var savedTeamsInProjects = DbContext.Team2Project.Where(x => projectsToActivate.Contains(x.ProjectId)) .Select(x => new { ProjectId = x.ProjectId, TeamId = x.TeamId }) .ToList() .GroupBy(x => x.ProjectId) .ToDictionary(x => x.Key, g => g.Select(s => s.TeamId).ToList()); foreach (var projectId in projectsToActivate) { if (!mix.Calendar.Projects.ContainsKey(projectId.ToString())) continue; var project = mix.Calendar.Projects[projectId.ToString()]; if (project == null || project.Scenario == null) continue; if (project.Teams == null) project.Teams = new List(); // we should preserve all teams for project and all its scenarios, we can remove only team allocations if (savedTeamsInProjects.ContainsKey(project.Id)) { var requiredTeams = savedTeamsInProjects[projectId].Where(x => !project.Teams.Any(s => s == x)).ToList(); project.Teams.AddRange(requiredTeams); } ActivateScenario(project.Scenario, mix.Name, project.Teams); DbContext.SaveChanges(); } } } public void ActivateScenario(ScenarioCalendarMixModel scenario, string name, List teams) { if (scenario == null) throw new ArgumentNullException("scenario"); var scenarioManager = (new ScenarioManager(DbContext)); var scenarioSaveModel = (ScenarioDetailsSnapshotSaveModel)scenario; if (scenarioSaveModel == null || scenarioSaveModel.Calendar == null) return; if ((scenario.Expenditures == null || scenario.Expenditures.Count <= 0) && scenario.Id != Guid.Empty) scenarioSaveModel.Calendar.Expenditures = (new ScenarioManager(DbContext)).GetFullAllocationInfoByScenario(scenario.Id, _userId); if (teams != null && teams.Count > 0) { scenarioSaveModel.TeamsInScenario = teams.Select(x => new TeamInScenarioModel() { TeamId = x }).ToList(); } // new scenario should be created with name equals to mix name scenarioSaveModel.Scenario.Name = name; var scenarioId = scenarioManager.Save(scenarioSaveModel); if (scenarioId == Guid.Empty) throw new InvalidOperationException("Save scenario method returned empty"); // save local rates for this scenario if (scenario.Rates != null && scenario.Rates.Count > 0) { var rates = scenarioManager.GetRatesFromModel(scenario.Rates); foreach(var rate in rates) { rate.Id = Guid.NewGuid(); rate.ParentId = scenarioId; rate.Type = (short)RateModel.RateType.Derived; } DbContext.Rates.AddRange(rates); } } public Dictionary GetFullAllocationInfoByScenario(string mixId, Guid scenarioId) { var allocations = new Dictionary(); if (scenarioId == Guid.Empty) return allocations; if (!string.IsNullOrWhiteSpace(mixId)) allocations = GetAllocations4Scenario(mixId, scenarioId); if (allocations == null || allocations.Count <= 0) allocations = (new ScenarioManager(DbContext)).GetFullAllocationInfoByScenario(scenarioId, _userId); return allocations; } public List GetMixesByUser() { Guid userId; if (!string.IsNullOrWhiteSpace(_userId) && Guid.TryParse(_userId, out userId) && userId != Guid.Empty) { return MongoDataContext.Mixes.Find(t => t.Users.Contains(userId)) .SortBy(o => o.Name) .ToListAsync() .GetAwaiter() .GetResult() .Select(t => new MixModelBase() { Id = t.Id.ToString(), Name = t.Name, StartDate = t.StartDate, EndDate = t.EndDate }).ToList(); } return new List(); } private Dictionary GetAllocations4Scenario(string mixId, Guid scenarioId) { var allocations = GetAllocations4Scenarios(mixId, new List() { scenarioId }); if (allocations == null || !allocations.ContainsKey(scenarioId)) return new Dictionary(); return allocations[scenarioId]; } private Dictionary> GetAllocations4Scenarios(string mixId, List scenarios) { var allocations = new Dictionary>(); if (string.IsNullOrWhiteSpace(mixId) || scenarios == null || scenarios.Count <= 0) return allocations; var mid = new ObjectId(mixId); var expenditureAllocations = MongoDataContext.ExpenditureAllocations .Find(t => t.MixId == mid && scenarios.Contains(t.ScenarioId)) .ToListAsync().GetAwaiter().GetResult() .GroupBy(x => x.ScenarioId) .ToDictionary(x => x.Key, g => g.ToList()); var teamAllocations = MongoDataContext.TeamAllocations .Find(t => t.MixId == mid && scenarios.Contains(t.ScenarioId)) .ToListAsync().GetAwaiter().GetResult() .GroupBy(x => x.ScenarioId) .ToDictionary(x => x.Key, g => g.ToList()); var resourceAllocations = MongoDataContext.ResourceAllocations .Find(t => t.MixId == mid && scenarios.Contains(t.ScenarioId)) .ToListAsync().GetAwaiter().GetResult() .GroupBy(x => x.ScenarioId) .ToDictionary(x => x.Key, g => g.ToList()); foreach (var scenarioId in scenarios) { if (!expenditureAllocations.ContainsKey(scenarioId)) continue; var expenditures = expenditureAllocations[scenarioId].ToDictionary(x => x.ExpCatId.ToString(), g => (ExpenditureDetail)g); allocations.Add(scenarioId, expenditures); if (!teamAllocations.ContainsKey(scenarioId)) continue; foreach (var expCatId in allocations[scenarioId].Keys) { var ec = allocations[scenarioId][expCatId]; ec.Teams = teamAllocations[scenarioId].Where(x => x.ExpCatId.ToString() == expCatId) .ToDictionary(x => x.TeamId, g => (ExpenditureDetailsTeam)g); if (!resourceAllocations.ContainsKey(scenarioId)) continue; foreach (var teamId in ec.Teams.Keys) { ec.Teams[teamId].Resources = resourceAllocations[scenarioId].Where(x => x.ExpCatId.ToString() == expCatId && x.TeamId == teamId) .ToDictionary(x => x.ResourceId, g => (TeamResource)g); } } } return allocations; } } }