using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data.Entity; using System.Linq; using System.Net; using System.Web.Mvc; using EnVisage.Code; using EnVisage.Code.Cache; using jQuery.DataTables.Mvc; using EnVisage.Models; using EnVisage.Code.BLL; using EnVisage.App_Start; using System.Web.Script.Serialization; using System.Text; using EnVisage.Code.Validation; using System.Threading.Tasks; namespace EnVisage.Controllers { public class TeamController : BaseController { #region Actions /// /// GET: /Teams/ /// /// Empty view [HttpGet] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Read)] public ActionResult Index() { return View(); } /// /// Returns JSON teams list with filters and sort for jQuery DataTables /// [HttpPost] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Read)] public JsonResult Index(JQueryDataTablesModel jQueryDataTablesModel) { int totalRecordCount; int searchRecordCount; var teams = GetTeams(startIndex: jQueryDataTablesModel.iDisplayStart, pageSize: jQueryDataTablesModel.iDisplayLength, sortedColumns: jQueryDataTablesModel.GetSortedColumns(), totalRecordCount: out totalRecordCount, searchRecordCount: out searchRecordCount, searchString: jQueryDataTablesModel.sSearch); return this.DataTablesJson(items: teams, totalRecords: totalRecordCount, totalDisplayRecords: searchRecordCount, sEcho: jQueryDataTablesModel.sEcho); } [HttpPost] public ActionResult GetPlanCapacitySimpleMode(string teamId) { Guid gTeamId = Guid.Parse(teamId); TeamManager teamManager = new TeamManager(DbContext); var categoriesInPlan = teamManager.GetPlannedCapacityCategoriesIds(Guid.Parse(teamId)); DateTime calendarMaxDate = (from c in DbContext.FiscalCalendars where c.Type == 0 && c.AdjustingPeriod == false orderby c.StartDate descending select c.EndDate).FirstOrDefault(); //first of all, we need to get categories + dates when category starts in either plan or reality - //these ranges allow us to work with this date range vs. entire fiscal calendar var subq = (from sdPlan in DbContext.ScenarioDetail join t in DbContext.Teams on sdPlan.ParentID equals t.PlannedCapacityScenarioId where t.Id == gTeamId select sdPlan).Union (from sdActual in DbContext.ScenarioDetail join t in DbContext.Teams on sdActual.ParentID equals t.ActualCapacityScenarioId where t.Id == gTeamId select sdActual); //In addition to that, we need a data set of details for both planned and actual capacities to check them for need increase or decrease while we iterating through FC' weeks Dictionary>> plannedCapacityDetails = (from sdPlan in DbContext.ScenarioDetail join t in DbContext.Teams on sdPlan.ParentID equals t.PlannedCapacityScenarioId where t.Id == gTeamId select sdPlan).GroupBy(sd => sd.ExpenditureCategoryId).ToDictionary(k1 => k1.Key, g1 => g1.GroupBy(sd2 => sd2.WeekEndingDate).ToDictionary(k2 => k2.Key, g2 => g2.ToList())); Dictionary>> actualCapacityDetails = (from sdPlan in DbContext.ScenarioDetail join t in DbContext.Teams on sdPlan.ParentID equals t.ActualCapacityScenarioId where t.Id == gTeamId select sdPlan).GroupBy(sd => sd.ExpenditureCategoryId).ToDictionary(k1 => k1.Key, g1 => g1.GroupBy(sd2 => sd2.WeekEndingDate).ToDictionary(k2 => k2.Key, g2 => g2.ToList())); var resultSet = GetPositionsSimpleMode(plannedCapacityDetails, actualCapacityDetails, categoriesInPlan, calendarMaxDate, subq, true); return Json(new { Result = true, data = resultSet }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult GetCapacitySimpleMode(string teamId, bool planned) { Guid gTeamId; Guid.TryParse(teamId, out gTeamId); Dictionary>> plannedCapacityDetails = new Dictionary>>(); Dictionary>> actualCapacityDetails = new Dictionary>>(); IQueryable subq = (new List()).AsQueryable(); TeamManager teamManager = new TeamManager(DbContext); var categoriesInPlan = teamManager.GetPlannedCapacityCategoriesIds(gTeamId); DateTime calendarMaxDate = DbContext.FiscalCalendars .Where(c => c.Type == 0 && c.AdjustingPeriod == false && c.NonWorking == 0) .OrderByDescending(c => c.StartDate) .Select(c => c.EndDate).FirstOrDefault(); if (!gTeamId.Equals(Guid.Empty)) { //first of all, we need to get categories + dates when category starts in either plan or reality - //these ranges allow us to work with this date range vs. entire fiscal calendar if (planned) subq = from sdPlan in DbContext.ScenarioDetail join t in DbContext.Teams on sdPlan.ParentID equals t.PlannedCapacityScenarioId where t.Id == gTeamId select sdPlan; else subq = from sdActual in DbContext.ScenarioDetail join t in DbContext.Teams on sdActual.ParentID equals t.ActualCapacityScenarioId where t.Id == gTeamId select sdActual; //In addition to that, we need a data set of details for both planned and actual capacities to check them for need increase or decrease while we iterating through FC' weeks if (planned) plannedCapacityDetails = (from sdPlan in DbContext.ScenarioDetail join t in DbContext.Teams on sdPlan.ParentID equals t.PlannedCapacityScenarioId where t.Id == gTeamId select sdPlan).GroupBy(sd => sd.ExpenditureCategoryId).ToDictionary(k1 => k1.Key, g1 => g1.GroupBy(sd2 => sd2.WeekEndingDate).ToDictionary(k2 => k2.Key, g2 => g2.ToList())); else plannedCapacityDetails = (from sdPlan in DbContext.ScenarioDetail join t in DbContext.Teams on sdPlan.ParentID equals t.ActualCapacityScenarioId where t.Id == gTeamId select sdPlan).GroupBy(sd => sd.ExpenditureCategoryId).ToDictionary(k1 => k1.Key, g1 => g1.GroupBy(sd2 => sd2.WeekEndingDate).ToDictionary(k2 => k2.Key, g2 => g2.ToList())); } var resultSet = GetPositionsSimpleMode(plannedCapacityDetails, actualCapacityDetails, categoriesInPlan, calendarMaxDate, subq); return Json(new { Result = true, data = resultSet }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult GetResourcesForFillDecrease(Guid teamId, Guid categoryId, DateTime dateToFire) { // Get all resources in the team PeopleResourcesManager mngr = new PeopleResourcesManager(this.DbContext); var resources = mngr.LoadPeopleResourcesByTeamNonStrict(teamId, false, dateToFire.ToUniversalTime()); // Filter resources by EC var resourcesOfEC = resources.ToList(); resourcesOfEC.RemoveAll(x => (x.StartDate >= dateToFire) || (x.EndDate.HasValue && (x.EndDate.Value <= dateToFire))); resourcesOfEC.RemoveAll(x => !x.ExpenditureCategoryId.Equals(categoryId)); resourcesOfEC.RemoveAll(x => !x.IsActiveEmployee); var result = resourcesOfEC.OrderBy(x => x.LastName).Select(x => new { Id = x.Id, Name = x.FirstName + " " + x.LastName, EndDate = !x.PermanentResource ? x.EndDate.Value : (DateTime?)null }); return Json(new { Result = true, data = result }, JsonRequestBehavior.AllowGet); } [HttpPost] public ActionResult FillDecreaseSubmit(Guid? resourceId, DateTime endDate) { PeopleResourcesManager resManager = new PeopleResourcesManager(DbContext); using (var transaction = DbContext.Database.BeginTransaction()) { var transactionId = DbContext.GetClientConnectionId(); var userId = User.Identity.GetID(); try { if (resourceId != null) { var resourceId1 = (Guid)resourceId; PeopleResourceModel model = resManager.LoadPeopleResource(resourceId1, DateTime.UtcNow.Date, false, false, false); model.PermanentResource = false; model.EndDate = endDate; model.ChangeCapacity = false; resManager.Save(model); DbContext.SaveChanges(); transaction.Commit(); Task.Run(() => AuditProxy.CommitHistoryChanges(transactionId, userId)); return Json(new { Result = true }, JsonRequestBehavior.AllowGet); } throw new ArgumentException("Resource to fill capacity decrease can't be null."); } catch (Exception ex) { transaction.Rollback(); AuditProxy.ClearHistoryChanges(transactionId); LogException(ex); return Json(new { Result = false }, JsonRequestBehavior.AllowGet); } } } [AreaSecurity(area = Areas.Teams, level = AccessLevel.Read)] public ActionResult Board() { // SA. ENV-804. Changed teams sorting var model = new TeamboardModel(); var teamMngr = new TeamManager(DbContext); // var teams = new TeamManager(DbContext).LoadTeamsWithResourcesByUser(Guid.Parse(User.Identity.GetID())); var teams = teamMngr.GetTeamsByUser(Guid.Parse(User.Identity.GetID())).Select(t => t.TeamId); if (!string.IsNullOrEmpty(Request.QueryString["teamId"])) { var tm = teams.FirstOrDefault(x => x.ToString() == Request.QueryString["teamId"].ToString()); if (tm.Equals(Guid.Empty)) return RedirectToAccessDenied(); var teamData = teamMngr.Load(tm, true); model.Teams.Add((TeamWithResourcesModel)teamData); } else { var teamsData = teamMngr.LoadTeams(teams); teamsData = teamsData.OrderBy(x => x.Name); model.Teams = teamsData.ToList().Select(x => (TeamWithResourcesModel)x).ToList(); } model.CalendarMaxDate = (from c in DbContext.FiscalCalendars where c.Type == (int)FiscalCalendarModel.FiscalYearType.Week && c.AdjustingPeriod == false && c.NonWorking == 0 orderby c.StartDate descending select c.EndDate).FirstOrDefault(); SetUserSelectedTeamId(model); return View(model); } [HttpPost] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Read)] [ValidateJsonAntiForgeryToken] public ActionResult GetTeamsByExpenditureCategory(Guid id) { if ((id == null) || (id.Equals(Guid.Empty))) throw new ArgumentNullException(nameof(id)); Guid UserId = SecurityManager.GetUserPrincipal(); TeamManager mngr = new TeamManager(DbContext); IQueryable teams = mngr.GetTeamsByExpenditureCategory(id, UserId); Dictionary result = teams.ToDictionary(k => k.Id.ToString(), v => v.Name); return Json(result); } [HttpGet] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Write)] public ActionResult Edit(Guid? id) { ViewBag.TeamId = id.ToString(); var model = new TeamModel(); try { if (id.HasValue) { var manager = new TeamManager(DbContext); model = manager.LoadTeamModel(id.Value) ?? new TeamModel(); } if (Guid.Empty.Equals(model.Id)) { model.UserId = new List(); model.WorkFlowContacts = new List(); model.NotificationContacts = new List(); model.NotificationWorkFlowStates = new List(); } else { model.UserId = DbContext.User2Team.Where(x => x.TeamId == model.Id).ToList().Select(x => Guid.Parse(x.UserId)).ToList(); model.WorkFlowContacts = (new WorkFlowManager(this.DbContext)).GetContactList(model.Id, WorkFlowContactNotificationType.None); model.NotificationContacts = (new WorkFlowManager(this.DbContext)).GetContactList(model.Id, WorkFlowContactNotificationType.TeamScenarioAdd); model.NotificationWorkFlowStates = (new WorkFlowManager(this.DbContext)).GetNotificationStates(model.Id, WorkFlowContactNotificationType.TeamScenarioAdd); } return PartialView("_editTeam", model); } catch (Exception exception) { LogException(exception); } return new HttpStatusCodeResult(HttpStatusCode.InternalServerError); } [HttpPost] [ValidateAjax] [ValidateAntiForgeryToken] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Write)] public ActionResult Edit(TeamModel model) { if (model != null && model.Id != Guid.Empty && ContentLocker.IsLock("Team", model.Id.ToString(), User.Identity.GetUserName())) { ModelState.AddModelError(string.Empty, @"This team is currently being updated by another user. Please attempt your edit again later."); return new FailedJsonResult(ModelState); } try { if (model == null) throw new ArgumentNullException(nameof(model)); model.TrimStringProperties(); if (model.ReportToId.HasValue && model.ReportToId == Guid.Empty) model.ReportToId = null; var context = new EnVisageEntities(); var manager = new TeamManager(context); manager.Save(model); context.SaveChanges(); new ProjectAccessCache().Invalidate(); ContentLocker.RemoveLock("Team", model.Id.ToString(), User.Identity.GetUserName()); return new SuccessJsonResult(); } catch (BLLException blEx) // handle any system specific error { // display error message if required if (blEx.DisplayError) ModelState.AddModelError(string.Empty, blEx.Message); else // if display not requried then display modal form with general error message { LogException(blEx); ModelState.AddModelError(string.Empty, @"Cannot save team. Try again later."); } } catch (Exception exception) // handle any unexpected error { LogException(exception); ModelState.AddModelError(string.Empty, @"Cannot save team. Try again later."); } return new FailedJsonResult(ModelState); } [HttpPost] [ValidateAntiForgeryToken] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Write)] public ActionResult Delete(Guid deleteTeamId) { try { if (deleteTeamId != Guid.Empty && ContentLocker.IsLock("Team", deleteTeamId.ToString(), User.Identity.GetUserName())) { ModelState.AddModelError(string.Empty, @"This team is currently being updated by another user. Please attempt your edit again later."); return new FailedJsonResult(ModelState); } var manager = new TeamManager(DbContext); var dbObj = manager.Load(deleteTeamId, false); if (dbObj == null) throw new InvalidOperationException( $"System cannot delete team {deleteTeamId} because it does not exist"); if (!DbContext.VW_TeamResource.Any(t => t.TeamId == dbObj.Id)) { manager.Delete(dbObj.Id); ContentLocker.RemoveLock("Team", dbObj.Id.ToString(), User.Identity.GetUserName()); return new SuccessJsonResult(); } ModelState.AddModelError(string.Empty, @"This team cannot be deleted, because it has some attached people resources."); } catch (BLLException blEx) { if (blEx.DisplayError) ModelState.AddModelError(string.Empty, blEx.Message); else { LogException(blEx); ModelState.AddModelError(string.Empty, @"Cannot delete team. Try again later."); } } catch (Exception exception) { LogException(exception); ModelState.AddModelError(string.Empty, @"Cannot delete team. Try again later."); } return new FailedJsonResult(ModelState); } [AreaSecurity(area = Areas.Teams, level = AccessLevel.Write)] public JsonResult ChangeCapacity(DateTime? startDate, DateTime? endDate, Guid expenditureCategoryId, int capacityValue, Guid teamId, bool? permanent) { #if DEBUG var watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); Logger.Debug("Start ChangeCapacity"); #endif var sb = new StringBuilder(); sb.AppendLine( $"TeamController.ChangeCapacity method. teamId:{teamId}, startDate:{startDate}, endDate{endDate}, ExpCatId:{expenditureCategoryId}, permanent:{permanent}, capacityValue: {capacityValue}"); Logger.Debug(sb); if (capacityValue == 0) return null; var rateManager = new RateManager(DbContext); var expCats = DbContext.ExpenditureCategory.Where(x => x.Id == expenditureCategoryId).ToDictionary(x => x.Id); var uomIds = expCats.Select(t => t.Value.UOMId); var uoms = DbContext.UOMs.Where(x => uomIds.Contains(x.Id)).ToDictionary(x => x.Id); var uomMultiplier = Utils.GetUOMMultiplier(expCats, uoms, expenditureCategoryId, false); capacityValue = (int)Math.Round(capacityValue / uomMultiplier); var teamManager = new TeamManager(DbContext); var team = teamManager.LoadTeamWithResourcesById(teamId); Scenario plannedCapacityScenario = GetOrCreatePlannedCapacityScenario(team); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"ChangeCapacity Start Load has taken {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"ChangeCapacity Start Load {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif List weekendings; if (permanent != null && (bool)permanent) { weekendings = (from c in DbContext.FiscalCalendars where c.Type == 0 && ((startDate >= c.StartDate && startDate <= c.EndDate) || (c.StartDate >= startDate)) orderby c.StartDate select c.EndDate).ToList(); } else { weekendings = (from c in DbContext.FiscalCalendars where c.Type == 0 && c.AdjustingPeriod == false && c.NonWorking == 0 && ((startDate >= c.StartDate && startDate <= c.EndDate) || (c.StartDate >= startDate && c.EndDate <= endDate) || (endDate >= c.StartDate && endDate <= c.EndDate)) orderby c.StartDate select c.EndDate).ToList(); } #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"ChangeCapacity List weekendings; has taken {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"ChangeCapacity List weekendings; {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif var rates = rateManager.GetRates(new List {expenditureCategoryId}, RateModel.RateType.Global); var sds = (from sd in DbContext.ScenarioDetail where sd.ParentID == team.PlannedCapacityScenarioId && sd.ExpenditureCategoryId == expenditureCategoryId && weekendings.Contains(sd.WeekEndingDate.Value) select sd).ToList(); var scenarioDetailToAddCollection = new List(); var scenarioDetailToDeleteCollection = new List(); foreach (var week in weekendings) { var sd = (from s in sds where s.WeekEndingDate == week select s).FirstOrDefault(); if (sd != null) { sd.Quantity += capacityValue; Logger.Debug("/Team/ChangeCapacity method. TeamId:{0}, capacityValue: {1}, sd.Quantity: {2}", teamId, capacityValue, sd.Quantity); if (sd.Quantity <= 0) { scenarioDetailToDeleteCollection.Add(sd); } else sd.Cost = sd.Quantity * rateManager.GetRateValue(rates, expenditureCategoryId, week); } else { if (capacityValue <= 0) continue; Logger.Debug("/Team/ChangeCapacity method. TeamId:{0}, capacityValue: {1}", teamId, capacityValue); var newSd = new ScenarioDetail { Id = Guid.NewGuid(), ParentID = plannedCapacityScenario.Id, Quantity = capacityValue }; newSd.Cost = newSd.Quantity * rateManager.GetRateValue(rates, expenditureCategoryId, week); newSd.ExpenditureCategoryId = expenditureCategoryId; newSd.WeekEndingDate = week; newSd.WeekOrdinal = 0; scenarioDetailToAddCollection.Add(newSd); } } DbContext.ScenarioDetail.AddRange(scenarioDetailToAddCollection); DbContext.ScenarioDetail.RemoveRange(scenarioDetailToDeleteCollection); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"ChangeCapacity foreach (var week in weekendings) has taken {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"ChangeCapacity foreach (var week in weekendings) {watch1.ElapsedMilliseconds} ms"); watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); #endif DbContext.BulkSaveChanges(); // taken from ResetCapacity action method, original comment for reason of the second call is: // AG: Second SaveChanges call made intentionally because it is much slower to search for max scenariodetails date in both DB and DbContext.Local collections than just a single simple query plannedCapacityScenario.EndDate = (from sd in DbContext.ScenarioDetail where sd.ParentID == plannedCapacityScenario.Id select sd.WeekEndingDate).Max(); if (plannedCapacityScenario.EndDate.HasValue) plannedCapacityScenario.EndDate = plannedCapacityScenario.EndDate.Value.Date; DbContext.SaveChanges(); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"ChangeCapacity All has taken {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"ChangeCapacity All {watch1.ElapsedMilliseconds} ms"); #endif return Json(new { Result = true, data = weekendings }, JsonRequestBehavior.AllowGet); } [AreaSecurity(area = Areas.Teams, level = AccessLevel.Write)] public JsonResult ResetCapacity(DateTime? startDate, DateTime? endDate, Guid? expenditureCategoryId, Guid teamId, bool? permanent) { #if DEBUG var watch1 = new System.Diagnostics.Stopwatch(); watch1.Start(); Logger.Debug("Start ResetCapacity"); #endif var sb = new StringBuilder(); sb.AppendLine($"TeamController.ResetCapacity method. teamId:{teamId}, startDate:{startDate}, endDate{endDate}, ExpCatId:{expenditureCategoryId}, permanent:{permanent}"); Logger.Debug(sb); var rateManager = new RateManager(DbContext); var teamManager = new TeamManager(DbContext); var team = teamManager.LoadTeamWithResourcesById(teamId); if (team.ActualCapacityScenarioId == null || Guid.Empty == team.ActualCapacityScenarioId) return Json(new { Result = false, data = "Cannot reset capacity - Team does not have Actual Capacity scenario" }, JsonRequestBehavior.AllowGet); Scenario plannedCapacityScenario = GetOrCreatePlannedCapacityScenario(team); var actualCapacityDetails = new Dictionary>>(); var existingPlannedDetails = new Dictionary>>(); List weekendings; if (permanent != null && (bool)permanent) weekendings = (from c in DbContext.FiscalCalendars.AsNoTracking() where c.Type == 0 && c.StartDate >= startDate && c.AdjustingPeriod == false && c.NonWorking == 0 orderby c.StartDate select c.EndDate).ToList(); else weekendings = (from c in DbContext.FiscalCalendars.AsNoTracking() where c.Type == 0 && c.StartDate >= startDate && c.EndDate <= endDate && c.AdjustingPeriod == false && c.NonWorking == 0 orderby c.StartDate select c.EndDate).ToList(); if (expenditureCategoryId.HasValue) { actualCapacityDetails.Add(expenditureCategoryId.Value, (from sd in DbContext.ScenarioDetail.AsNoTracking() where sd.ParentID == team.ActualCapacityScenarioId && sd.ExpenditureCategoryId == expenditureCategoryId && weekendings.Contains(sd.WeekEndingDate.Value) select sd) .GroupBy(sd => sd.WeekEndingDate).ToDictionary(key => key.Key, group => group.ToList())); existingPlannedDetails.Add(expenditureCategoryId.Value, (from sd in DbContext.ScenarioDetail where sd.ParentID == team.PlannedCapacityScenarioId && sd.ExpenditureCategoryId == expenditureCategoryId && weekendings.Contains(sd.WeekEndingDate.Value) select sd) .GroupBy(sd => sd.WeekEndingDate).ToDictionary(key => key.Key, group => group.ToList())); } else { actualCapacityDetails = (from sd in DbContext.ScenarioDetail.AsNoTracking() where sd.ParentID == team.ActualCapacityScenarioId && weekendings.Contains(sd.WeekEndingDate.Value) select sd) .GroupBy(sd => sd.ExpenditureCategoryId.Value).ToDictionary(key => key.Key, group => group.GroupBy(x => x.WeekEndingDate).ToDictionary(k2 => k2.Key, g2 => g2.ToList())); existingPlannedDetails = (from sd in DbContext.ScenarioDetail where sd.ParentID == team.PlannedCapacityScenarioId && weekendings.Contains(sd.WeekEndingDate.Value) select sd) .GroupBy(sd => sd.ExpenditureCategoryId.Value).ToDictionary(key => key.Key, group => group.GroupBy(x => x.WeekEndingDate).ToDictionary(k2 => k2.Key, g2 => g2.ToList())); } //get rates for actual capacity categories var actualCategoriesIds = actualCapacityDetails.Keys.ToArray(); Dictionary> categoriesRates = DbContext.Rates.AsNoTracking().Where(r => actualCategoriesIds.Contains(r.ExpenditureCategoryId) && r.Type == (int)RateModel.RateType.Global) .GroupBy(x => x.ExpenditureCategoryId).ToDictionary(key => key.Key, group => group.ToList()); var scenarioDetailToDeleteCollection = new List(); var sdToAdd = new List(); foreach (Guid categoryId in actualCapacityDetails.Keys) { Dictionary> actualCapacities = actualCapacityDetails.ContainsKey(categoryId) ? actualCapacityDetails[categoryId] : new Dictionary>(); //We do not need UoM stuff here as we can just copy values from Actual Capacity week details into Planned Capacity week details Dictionary> existingDetails = existingPlannedDetails.ContainsKey(categoryId) ? existingPlannedDetails[categoryId] : new Dictionary>(); foreach (var week in weekendings) { ScenarioDetail existingD = existingDetails.ContainsKey(week) ? existingDetails[week].First() : null; decimal capacityValue = actualCapacities.ContainsKey(week) ? actualCapacities[week].FirstOrDefault().Quantity ?? 0 : 0; if (existingD != null) { if (capacityValue > 0) { existingD.Quantity = capacityValue; existingD.Cost = existingD.Quantity * rateManager.GetRateValue(categoriesRates, categoryId, week); } else { scenarioDetailToDeleteCollection.Add(existingD); } //we also need to remove this existing detail record from the main dicionary to mark it as "processed". //we should delete all remaining items because they do not get into our "new" list of categories for planned capacity - //e.g. old planned capacity had more categories than actual capacity has existingPlannedDetails[categoryId].Remove(week); } else { if (capacityValue <= 0) continue; var newSD = new ScenarioDetail { Id = Guid.NewGuid(), ParentID = plannedCapacityScenario.Id, Quantity = capacityValue, Cost = capacityValue * rateManager.GetRateValue(categoriesRates, categoryId, week), ExpenditureCategoryId = categoryId, WeekEndingDate = week, WeekOrdinal = 0 }; sdToAdd.Add(newSD); } } } var removedDetails = scenarioDetailToDeleteCollection; foreach (var categoryId in existingPlannedDetails.Keys) foreach (var weekEnding in existingPlannedDetails[categoryId].Keys) removedDetails.AddRange(existingPlannedDetails[categoryId][weekEnding]); DbContext.ScenarioDetail.RemoveRange(removedDetails); DbContext.ScenarioDetail.AddRange(sdToAdd); //We do not save scenario details history here - it is too time consuming on big calendars DbContext.BulkSaveChanges(); //AG: Second SaveChanges call made intentionally because it is much slower to search for max scenariodetails date in both DB and DbContext.Local collections than just a single simple query //Plus, upper call does not save history and it is good to at least save the fact of scenario change plannedCapacityScenario.EndDate = (from sd in DbContext.ScenarioDetail where sd.ParentID == plannedCapacityScenario.Id select sd.WeekEndingDate).Max(); if (plannedCapacityScenario.EndDate.HasValue) plannedCapacityScenario.EndDate = plannedCapacityScenario.EndDate.Value.Date; DbContext.SaveChanges(); #if DEBUG watch1.Stop(); System.Diagnostics.Debug.WriteLine($"ResetCapacity All has taken {watch1.ElapsedMilliseconds} ms"); Logger.Debug($"ResetCapacity All {watch1.ElapsedMilliseconds} ms"); #endif return Json(new { Result = true, data = "" }, JsonRequestBehavior.AllowGet); } [HttpPost] [AreaSecurity(area = Areas.Teams, level = AccessLevel.Read)] public ActionResult GetTeamResources(Guid teamId) { if (teamId.Equals(Guid.Empty)) return new HttpStatusCodeResult(HttpStatusCode.BadRequest); PeopleResourcesManager mngr = new PeopleResourcesManager(DbContext); List resources = mngr.LoadPeopleResourcesByTeamNonStrict(teamId, true, DateTime.UtcNow).OrderBy(x => x.LastName).Select(t => new TeamResourceModel() { Id = t.Id, Name = t.FirstName + " " + t.LastName, IsActive = t.IsActiveEmployee, StartDate = t.StartDate.ToShortDateString(), EndDate = t.EndDate?.ToShortDateString() ?? "", ExpenditureCategory = t.ExpenditureCategory.GetView().ExpCategoryWithCcName, ResourceScenarioAllocationsInfo = t.ResourceScenarioAllocationsInfo, ResourceActualsInfo = t.ResourceActualsInfo, ResourceMixAllocationsInfo = t.ResourceMixAllocationsInfo }).ToList(); return Json(resources); } [HttpPost] [ValidateJsonAntiForgeryToken] public ActionResult GetTeamsById(List ids) { try { var teamManager = new TeamManager(DbContext); var fiscalCalendar = (new FiscalCalendarManager(DbContext)).GetFiscalCalendar(FiscalCalendarModel.FiscalYearType.Week, null, null, true, null, false); var teams = teamManager.GetTeamsByUserFiltered(User.Identity.GetID(), ids, null, null); var teamModels = teamManager.GetTeamsInfo(teams, fiscalCalendar) .ToDictionary(x => x.Id.ToString()); return BigJson(teamModels); } catch (Exception exception) // handle any unexpected error { LogException(exception); } return new HttpStatusCodeResult(HttpStatusCode.InternalServerError); } #endregion #region Private Methods private List GetPositionsSimpleMode(Dictionary>> plannedCapacityDetails, Dictionary>> actualCapacityDetails, Guid?[] categoriesInPlan, DateTime CalendarMaxDate, IQueryable subq, bool withoutSupperExpCat = false) { ExpenditureCategoryManager categoryManager = new ExpenditureCategoryManager(DbContext); var allCategories = categoryManager.GetExpenditureDetails(!withoutSupperExpCat); var categoriesDates = (from r in subq group r by r.ExpenditureCategoryId into rg select new { ExpenditureCategoryId = rg.Key, StartDate = rg.Min(z => z.WeekEndingDate), EndDate = rg.Max(z => z.WeekEndingDate) }).ToDictionary(c => c.ExpenditureCategoryId); List resultSet = new List(); foreach (var catId in allCategories.Keys) { var cat = allCategories[catId]; PlannedCapacitySimpleCategory resultItem = new PlannedCapacitySimpleCategory { Id = cat.ExpenditureCategoryId, Name = cat.ExpenditureCategoryName, InPlan = categoriesInPlan.Contains(cat.ExpenditureCategoryId) }; var catDates = categoriesDates.ContainsKey(cat.ExpenditureCategoryId) ? categoriesDates[cat.ExpenditureCategoryId] : null; if (catDates != null) { resultItem.Positions = new List(); Dictionary> categoryActuals = actualCapacityDetails.ContainsKey(cat.ExpenditureCategoryId) ? actualCapacityDetails[cat.ExpenditureCategoryId] : new Dictionary>(); Dictionary> categoryPlans = plannedCapacityDetails.ContainsKey(cat.ExpenditureCategoryId) ? plannedCapacityDetails[cat.ExpenditureCategoryId] : new Dictionary>(); //AG: I did not generate single FiscalCalendar weeks set for all categories intentionally as once we change FiscalCalendar generation to on-the-fly calculation, it //does not make sense to keep redundant data for all categories as we can simply re-generate FC weeks for each of them var wkRanges = (from c in DbContext.FiscalCalendars where c.Type == (int)FiscalCalendarModel.FiscalYearType.Week && c.EndDate >= catDates.StartDate && c.EndDate <= catDates.EndDate && c.AdjustingPeriod == false && c.NonWorking == 0 orderby c.StartDate select new { c.StartDate, c.EndDate }).ToList(); decimal currentNeed = 0; decimal newNeed = 0; DateTime prevEndDate = DateTime.MinValue; Stack positionsStack = new Stack(); foreach (var range in wkRanges) { newNeed = 0; ScenarioDetail weeklyPlan = categoryPlans.ContainsKey(range.EndDate) ? categoryPlans[range.EndDate].FirstOrDefault() : null; ScenarioDetail weeklyActual = categoryActuals.ContainsKey(range.EndDate) ? categoryActuals[range.EndDate].FirstOrDefault() : null; if (weeklyPlan != null) newNeed = weeklyPlan.Quantity.HasValue ? Math.Round(weeklyPlan.Quantity.Value) : 0; if (weeklyActual != null) newNeed -= weeklyActual.Quantity.HasValue ? Math.Round(weeklyActual.Quantity.Value) : 0; if (currentNeed != newNeed) { #region Positions management if (currentNeed > 0) { if (newNeed > currentNeed) { //open new position(s) while (currentNeed < newNeed) { positionsStack.Push(new PlannedCapacityResourcePosition() { StartDate = range.StartDate, Need = 1 }); currentNeed += cat.UOMValue; } } else { //close latest position(s) while we are above the zero, then open new negative positions while (currentNeed > newNeed) { if (currentNeed > 0) { if (positionsStack.Count == 0) throw new InvalidOperationException("There is a bug somewhere in the planned capacity positions builder algorithm - the stack should not be empty here - positive position close"); PlannedCapacityResourcePosition pos = positionsStack.Pop(); if (pos.Need < 0) throw new InvalidOperationException("There is a bug somewhere in the planned capacity positions builder algorithm - previously opened position should be positive"); pos.EndDate = prevEndDate < CalendarMaxDate ? prevEndDate : (DateTime?)null; resultItem.Positions.Add(pos); } else { positionsStack.Push(new PlannedCapacityResourcePosition() { StartDate = range.StartDate, Need = -1 }); } currentNeed -= cat.UOMValue; } } } else if (currentNeed < 0) { if (newNeed < currentNeed) { //open new negative position(s) while (currentNeed > newNeed) { positionsStack.Push(new PlannedCapacityResourcePosition() { StartDate = range.StartDate, Need = -1 }); currentNeed -= cat.UOMValue; } } else { //close latest negative position(s) while we less than 0; open new positive position when we passed 0 while (currentNeed < newNeed) { if (currentNeed < 0) { if (positionsStack.Count == 0) throw new InvalidOperationException("There is a bug somewhere in the planned capacity positions builder algorithm - the stack should not be empty here - negative position close"); PlannedCapacityResourcePosition pos = positionsStack.Pop(); if (pos.Need > 0) throw new InvalidOperationException("There is a bug somewhere in the planned capacity positions builder algorithm - previously opened position should be negative"); pos.EndDate = prevEndDate < CalendarMaxDate ? prevEndDate : (DateTime?)null; resultItem.Positions.Add(pos); } else { positionsStack.Push(new PlannedCapacityResourcePosition() { StartDate = range.StartDate, Need = 1 }); } currentNeed += cat.UOMValue; } } } else //currentNeed == 0 { if (newNeed > 0) { //open new position(s) while (currentNeed < newNeed) { positionsStack.Push(new PlannedCapacityResourcePosition() { StartDate = range.StartDate, Need = 1 }); currentNeed += cat.UOMValue; } } else { //open new negative position(s) while (currentNeed > newNeed) { positionsStack.Push(new PlannedCapacityResourcePosition() { StartDate = range.StartDate, Need = -1 }); currentNeed -= cat.UOMValue; } } } currentNeed = newNeed; #endregion } prevEndDate = range.EndDate; } //we may have several positions still in stack as we retrieve only a significant portion of fiscal calendar. //we just need to put all of these positions into the list while (positionsStack.Count > 0) { PlannedCapacityResourcePosition pos = positionsStack.Pop(); pos.EndDate = wkRanges[wkRanges.Count - 1].EndDate < CalendarMaxDate ? wkRanges[wkRanges.Count - 1].EndDate : (DateTime?)null; resultItem.Positions.Add(pos); } } resultSet.Add(resultItem); } return resultSet; } private IList GetTeams(int startIndex, int pageSize, ReadOnlyCollection sortedColumns, out int totalRecordCount, out int searchRecordCount, string searchString) { var query = DbContext.Teams.Select(team => new ListTeam { Id = team.Id, Name = team.Name, Company = team.Company.Name, CostCenter = team.CreditDepartment.Name, CostCenterNumber = team.CreditDepartment.CreditNumber, ColumnCostCenter = team.CreditDepartment.Name + (string.IsNullOrEmpty(team.CreditDepartment.CreditNumber) ? string.Empty : " (" + team.CreditDepartment.CreditNumber + ")"), ReportTo = team.Contact.FirstName + " " + team.Contact.LastName, IsResourcesAttached = DbContext.PeopleResource2Team.Any(s => s.TeamId == team.Id), Users = DbContext.User2Team .Where(s => s.TeamId == team.Id) .Select(x => x.AspNetUser.FirstName + " " + x.AspNetUser.LastName) .ToList() }); //filter if (!string.IsNullOrWhiteSpace(searchString)) { query = query.Where(c => c.Name.ToLower().Contains(searchString.ToLower())); } //sort foreach (var sortedColumn in sortedColumns) { switch (sortedColumn.PropertyName) { case "Id": query = sortedColumn.Direction == SortingDirection.Ascending ? query.OrderBy(c => c.Id) : query.OrderByDescending(c => c.Id); break; case "Company": query = sortedColumn.Direction == SortingDirection.Ascending ? query.OrderBy(c => c.Company) : query.OrderByDescending(c => c.Company); break; case "ColumnCostCenter": query = sortedColumn.Direction == SortingDirection.Ascending ? query.OrderBy(c => c.CostCenter).ThenBy(c => c.CostCenterNumber) : query.OrderByDescending(c => c.CostCenter).ThenByDescending(c => c.CostCenterNumber); break; case "View": query = sortedColumn.Direction == SortingDirection.Ascending ? query.OrderBy(c => c.GLAccount) : query.OrderByDescending(c => c.GLAccount); break; case "ReportTo": query = sortedColumn.Direction == SortingDirection.Ascending ? query.OrderBy(c => c.ReportTo) : query.OrderByDescending(c => c.ReportTo); break; default: query = sortedColumn.Direction == SortingDirection.Ascending ? query.OrderBy(c => c.Name) : query.OrderByDescending(c => c.Name); break; } } totalRecordCount = DbContext.Teams.Count(); searchRecordCount = query.Count(); return query.Skip(startIndex).Take(pageSize).ToList(); } /// /// Set initially selected team according to user preferences /// /// /// SA. ENV-815 private void SetUserSelectedTeamId(TeamboardModel model) { Guid selectedTeamId = Guid.Empty; string userIdAsText = User.Identity.GetID(); Guid userId = new Guid(userIdAsText); string pageUrl = HttpContext.Request.Url.AbsolutePath; var prefRecords = DbContext.UserPreferences.Where(x => x.UserId.Equals(userId) && x.Url.Equals(pageUrl, StringComparison.InvariantCultureIgnoreCase) && x.Section.Equals("teamsBlock", StringComparison.InvariantCultureIgnoreCase)); if (prefRecords.Any()) { string prefData = prefRecords.First().Data; JavaScriptSerializer ser = new JavaScriptSerializer(); var data = ser.Deserialize>(prefData); var selectedTeamPrefs = data.Where(x => x.Key.Equals("pageSelectedTeam", StringComparison.InvariantCultureIgnoreCase) && x.Value.Length > 0).ToList(); if (selectedTeamPrefs.Count > 0) Guid.TryParse(selectedTeamPrefs.First().Value, out selectedTeamId); } if (model.Teams.Count > 0) { List teamIds = model.Teams.Select(x => x.Id).ToList(); if (!selectedTeamId.Equals(Guid.Empty) && teamIds.Contains(selectedTeamId)) { model.Id = selectedTeamId; model.SelectedTeamName = model.Teams.Where(x => x.Id.Equals(selectedTeamId)).Select(x => x.Name).First(); } else { model.Id = teamIds.First(); model.SelectedTeamName = model.Teams.First().Name; } } } //private List FillMissingWeeends(List list, IEnumerable weekEnds) //{ // if (list.Count < weekEnds.Count()) // { // var result = weekEnds.Except(list.Select(x => x[0])); // list.AddRange(result.Select(x => new long[] { x, 0 })); // } // return list.OrderBy(x => x[0]).ToList(); //} private Scenario GetOrCreatePlannedCapacityScenario(TeamWithResourcesModel team) { var scenarioManager = new ScenarioManager(DbContext); Scenario scen; if (team.PlannedCapacityScenarioId == null || team.PlannedCapacityScenarioId == Guid.Empty) { scen = new Scenario { Name = team.Name.Trim() + " Planned Capacity", Type = (int)ScenarioType.TeamPlannedCapacity, Id = Guid.NewGuid() }; team.PlannedCapacityScenarioId = scen.Id; DbContext.Scenarios.Add(scen); DbContext.SaveChanges(); } else scen = scenarioManager.Load(team.PlannedCapacityScenarioId, isReadOnly: false); return scen; } #endregion #region Models /// /// An UI representation of Team to be displayed as list items /// public class ListTeam { public Guid Id { get; set; } public string Name { get; set; } public List Users { get; set; } public string Company { get; set; } public string CostCenter { get; set; } public string CostCenterNumber { get; set; } public string ColumnCostCenter { get; set; } public string GLAccount { get; set; } public string ReportTo { get; set; } public bool IsResourcesAttached { get; set; } } /// /// An UI representation of category and open positions for planned capacity simple mode /// public class PlannedCapacitySimpleCategory { public Guid Id { get; set; } public string Name { get; set; } public bool InPlan { get; set; } public List Positions { get; set; } } public class PlannedCapacityResourcePosition { public int Need { get; set; } public DateTime StartDate { get; set; } public DateTime? EndDate { get; set; } public long StartDateMs => Utils.ConvertToUnixDate(StartDate); public long EndDateMs => EndDate.HasValue ? Utils.ConvertToUnixDate(EndDate.Value) : 0; } class TeamResourceModel { public Guid Id; public string Name; public bool IsActive; public string StartDate; public string EndDate; public string ExpenditureCategory; public PeopleResourceAllocationsInfoModel ResourceScenarioAllocationsInfo; public PeopleResourceAllocationsInfoModel ResourceActualsInfo; public PeopleResourceAllocationsInfoModel ResourceMixAllocationsInfo; } #endregion } }