using System; using System.Collections.Generic; using System.Linq; using System.Linq.Dynamic; using System.Web.Mvc; using EnVisage.Models; using EnVisage.Code; using EnVisage.Code.Charts; using System.Collections; using EnVisage.Code.ForecastDashboard; using System.Net; using EnVisage.Code.BLL; namespace EnVisage.Controllers { [Authorize] public class ForecastDashboardController : BaseController { #region Actions /// /// Direct GET to the page - returns main view /// public ActionResult Index(string menuId, string pageModel, string additionalFilters) { return PartialView("_forecastDashboard", new ForecastDashboardOptionsModel { MenuId = menuId, // Added to trace the source page, where forecast widget to be shown SourcePageModel = !String.IsNullOrEmpty(pageModel) ? pageModel : String.Empty, AdditionalFilterParams = additionalFilters }); } /// /// Direct POST request to the page - returns main part of dashboard table - active scenarios /// [HttpPost] public JsonResult GetTable(DataSourceRequest request, ForecastDashboardFilterModel filter) { int totalRecordCount; int searchRecordCount; //var sortedColumns = request.Sort != null ? request.Sort.Select(x => new SortedColumn(x.Field, x.Dir)).ToList() : null; //var sortedColumnsCollection = sortedColumns != null ? new ReadOnlyCollection(sortedColumns) : null; string ordering = string.Empty; var manager = new ForecastDashboardManager(DbContext); var userId = SecurityManager.GetUserPrincipal(); var scenarios = manager.GetScenarios(userId, out totalRecordCount, out searchRecordCount, "", filter); if (request.Group != null) { ordering = string.Join(",", request.Group.Select(GetOrderDescriptor)); } if (request.Sort != null) { var sort = string.Join(",", request.Sort.Select(GetOrderDescriptor)); ordering = string.IsNullOrEmpty(ordering) ? sort : string.Join(",", ordering, sort); } if (!string.IsNullOrEmpty(ordering)) { scenarios = scenarios.OrderBy(ordering); } var distinctScenarios = scenarios.GroupBy(x => x.Id) .Select(x => x.FirstOrDefault()) .ToList(); var pageItems = scenarios.Skip((request.Page - 1) * request.PageSize).Take(request.PageSize); var gridData = new { Total = scenarios.Count(), Aggregates = GetAggregates(distinctScenarios), Groups = GetGroups2(scenarios, pageItems, request.Group), Data = request.Group == null || !request.Group.Any() ? pageItems : null }; return Json(gridData, JsonRequestBehavior.AllowGet); } /// /// Adds Scenario to the given Scenario Group /// [HttpPost] public JsonResult CopyScenarioToGroup(Guid? scenarioId, Guid? groupId) { try { if (!scenarioId.HasValue || !groupId.HasValue) throw new Exception("ScenarioId and GroupId can't be null."); if (scenarioId == Guid.Empty || groupId == Guid.Empty) throw new Exception("ScenarioId and GroupId can't be empty."); CopyToGroup(scenarioId.Value, groupId.Value); return Json(new { Status = "Ok" }); } catch (Exception ex) { return Json(new { Status = "Error", ErrorMsg = ex.Message, ErrorDetails = ex.ToString() }); } } /// /// Removes Scenario from the given Scenario Group /// [HttpPost] public JsonResult ExtractFromGroup(Guid? scenarioId, Guid? groupId) { try { if (!scenarioId.HasValue || !groupId.HasValue) throw new Exception("ScenarioId and GroupId can't be null."); if (scenarioId == Guid.Empty || groupId == Guid.Empty) throw new Exception("ScenarioId and GroupId can't be empty."); DeleteFromGroup(scenarioId.Value, groupId.Value); return Json(new { Status = "Ok" }); } catch (Exception ex) { return Json(new { Status = "Error", ErrorMsg = ex.Message, ErrorDetails = ex.ToString() }); } } [HttpPost] public ActionResult GetBubbleChartData(ForecastDashboardChartFilterModel filter) { if (!filter.StartDate.HasValue) filter.StartDate = new DateTime(DateTime.Today.Year, 1, 1); if (!filter.EndDate.HasValue) filter.EndDate = new DateTime(DateTime.Today.Year, 12, 31); var mode = ForecastDashboardMode.MainDashboard; if (filter.AdditionalParams != null && filter.AdditionalParams.ContainsKey("mode")) { filter.Teams = new List(); filter.Views = new List(); if (filter.AdditionalParams["mode"] == "team" && filter.AdditionalParams.ContainsKey("teamId")) { mode = ForecastDashboardMode.TeamForecast; var teamId = Guid.Empty; if (Guid.TryParse((string)filter.AdditionalParams["teamId"], out teamId)) filter.Teams.Add(teamId); } if (filter.AdditionalParams["mode"] == "view" && filter.AdditionalParams.ContainsKey("viewId")) { mode = ForecastDashboardMode.ViewForecast; var viewId = Guid.Empty; if (Guid.TryParse((string)filter.AdditionalParams["viewId"], out viewId)) filter.Views.Add(viewId); } } string defaultColor; DashboardChartManager.Data[] statisticData; using (var manager = new DashboardChartManager()) { var userId = SecurityManager.GetUserPrincipal(); defaultColor = Utils.GetDefaultProjectColor(); statisticData = manager.GetBubbleChartData(filter.StartDate.Value, filter.EndDate.Value, userId, filter.Type, filter.ProjectStatuses, filter.ProjectTypes, mode, filter.Teams, filter.Views, filter.Companies, filter.IsLaborMode, filter.FilterGroups, filter.StrategicGoals, filter.Tags, filter.Clients) .ToArray(); } var chartData = from item in statisticData group item by new { item.ProjectId, item.ProjectPriority, item.ProjectName, item.StardDate, item.EndDate, item.ProjectColor, item.ParentProjectColor, item.PerformanceRedThreshold, item.PerformanceYellowThreshold, item.ParentProjectId, item.ParentProjectName, item.VariationPercent } into g let dataItem = g.Key let middleDate = Utils.GetMiddleDate(dataItem.StardDate.Value, dataItem.EndDate.Value) select new { middleDate = middleDate.ToString("MM/dd/yyyy"), middleUnixDate = Utils.ConvertToUnixDate(middleDate), projectId = dataItem.ParentProjectId ?? dataItem.ProjectId, projectName = dataItem.ProjectName + (dataItem.ParentProjectId.HasValue ? ": " + dataItem.ParentProjectName : string.Empty), projectPriority = dataItem.ProjectPriority, projectColor = Utils.GetProjectColor(dataItem.ProjectColor, dataItem.ParentProjectColor, defaultColor), performanceColor = GetColor(dataItem.PerformanceYellowThreshold, dataItem.PerformanceRedThreshold, dataItem.VariationPercent), quantity = Math.Round((decimal)g.Sum(x => x.quantity)), cost = g.Sum(x => x.cost) }; long minDate = -1; long maxDate = -1; var chartDataArr = chartData.ToArray(); if (chartDataArr.Any()) { minDate = chartDataArr.Min(x => x.middleUnixDate); maxDate = chartDataArr.Max(x => x.middleUnixDate); if (minDate == maxDate) { long longDay = 1000 * 60 * 60 * 24; minDate -= (longDay * 182); maxDate += (longDay * 183); } for (var i = 0; i < chartDataArr.Length; i++) { var middleDate1 = chartDataArr[i].middleDate; var counter = i + 1; var offset = 1; for (var j = counter; j < chartDataArr.Length; j++) { var middleDate2 = chartDataArr[j].middleDate; if (middleDate1 == middleDate2) { chartDataArr[j] = new { middleDate = Convert.ToDateTime(chartDataArr[j].middleDate).AddDays(offset).ToString("MM/dd/yyyy"), middleUnixDate = chartDataArr[j].middleUnixDate, projectId = chartDataArr[j].projectId, projectName = chartDataArr[j].projectName, projectPriority = chartDataArr[j].projectPriority, projectColor = chartDataArr[j].projectColor, performanceColor = chartDataArr[j].performanceColor, quantity = chartDataArr[j].quantity, cost = chartDataArr[j].cost }; offset++; } } } } var result = new { chartData = chartDataArr.OrderByDescending(q => q.quantity), minimumDate = minDate, maximumDate = maxDate }; return Json(result); } /// /// Returns data for main chart /// [HttpPost] public ActionResult GetGraphData(ForecastDashboardChartFilterModel filter) { if (filter == null) return new HttpStatusCodeResult(HttpStatusCode.BadRequest); try { if (!filter.StartDate.HasValue) filter.StartDate = new DateTime(DateTime.Today.Year, 1, 1); if (!filter.EndDate.HasValue) filter.EndDate = new DateTime(DateTime.Today.Year, 12, 31); var mode = ForecastDashboardMode.MainDashboard; if (filter.AdditionalParams != null && filter.AdditionalParams.ContainsKey("mode")) { filter.Teams = new List(); filter.Views = new List(); if (filter.AdditionalParams["mode"] == "team" && filter.AdditionalParams.ContainsKey("teamId")) { mode = ForecastDashboardMode.TeamForecast; Guid teamId; if (Guid.TryParse(filter.AdditionalParams["teamId"], out teamId)) filter.Teams.Add(teamId); } if (filter.AdditionalParams["mode"] == "view" && filter.AdditionalParams.ContainsKey("viewId")) { mode = ForecastDashboardMode.ViewForecast; Guid viewId; if (Guid.TryParse(filter.AdditionalParams["viewId"], out viewId)) filter.Views.Add(viewId); } } Dictionary result = new Dictionary(); using (var dashboardChartManager = new DashboardChartManager()) { var data = dashboardChartManager.GetData(filter.StartDate.Value, filter.EndDate.Value, Guid.Parse(User.Identity.GetID()), filter.Type, filter.ProjectStatuses, filter.ProjectTypes, filter.Clients, mode, filter.Teams, filter.Views, filter.Companies, filter.IsLaborMode, filter.FilterGroups, filter.StrategicGoals, filter.Tags); if ((filter.ChartType & DashboardChartType.ProjectsByStatus) == DashboardChartType.ProjectsByStatus) result.Add(DashboardChartType.ProjectsByStatus.ToString(), dashboardChartManager.GetProjectsByStatusGraphModel(data)); if ((filter.ChartType & DashboardChartType.Optimuse) == DashboardChartType.Optimuse) result.Add(DashboardChartType.Optimuse.ToString(), dashboardChartManager.GetOptimuseData(data)); } return Json(result); } catch (Exception ex) { LogException(ex); return new HttpStatusCodeResult(HttpStatusCode.InternalServerError); } } /// /// Returns data for pie chart /// [HttpPost] public JsonResult GetClassPieDataForNewDashboard(ForecastDashboardNewPieChartFilterModel filter) { var userId = Guid.Parse(User.Identity.GetID()); DoubleDonutChartData data; using (var manager = new DashboardChartManager()) { data = manager.GetPieData(userId, filter); } return Json(data); } /// /// Returns data for Top-5 projects / project types chart /// [HttpPost] public JsonResult GetPerformanceGraphData(ForecastDashboardPerformanceGraphFilterModel filter) { // Take data for specified time point (by default, client sends current date in his own time zone) var userId = Guid.Parse(User.Identity.GetID()); var mode = ForecastDashboardMode.MainDashboard; var timePoint = filter.TimePoint > 0 ? Utils.ConvertFromUnixDate(filter.TimePoint).Date : DateTime.UtcNow.Date; Dictionary graphData; using (var mngr = new DashboardChartManager()) { switch (filter.GraphMode) { case 1: // Top 5 cost projects performance graphData = mngr.GetPerformanceGraphDataByProjects(mode, userId, timePoint, filter); break; case 2: // Top 5 project types performance graphData = mngr.GetPerformanceGraphDataByProjectClass(mode, userId, timePoint, filter); break; default: // Invalid data type requested graphData = new Dictionary(); break; } } return Json(graphData); } [HttpGet] public ActionResult CheckActiveScenario(Guid projectId) { var projectManager = new ProjectManager(DbContext); var project = projectManager.GetProjectByUser(Guid.Parse(User.Identity.GetID()), projectId); if (project == null) return HttpNotFound(); return Json(new { activeScenarioId = project.ActiveScenarioId }, JsonRequestBehavior.AllowGet); } #endregion #region Private Methods private string GetOrderDescriptor(Sort sort) { return GetOrderDescriptor(sort.Field, sort.Dir); } private string GetOrderDescriptor(Group group) { return GetOrderDescriptor(group.Field, group.Dir); } private string GetOrderDescriptor(string field, string dir) { switch (dir) { case "asc": return field; case "desc": return field + " descending"; } return null; } //private dynamic GetGroupItems(GroupResult groupResult) //{ // if (groupResult == null || groupResult.Count == 0) // { // return null; // } // var hasSubgroups = groupResult.SubGroups != null && groupResult.SubGroups.Any(); // var groupItems = hasSubgroups ? groupResult.SubGroups.Select(GetGroupItems) : groupResult.Items; // return new // { // field = groupResult.Field, // value = groupResult.Key, // items = groupItems, // aggregates = GetAggregates(groupResult.Items.AsQueryable().Select("it")), // hasSubgroups // }; //} //TODO: get aggregates from client (see GetGroups2) and translate to: new( new(it.Count()) as Id, new(it.Min(it.StartDate)) as StartDate ... ) private dynamic GetAggregates(IEnumerable items) { var result = new { Id = new { count = items.Count() }, StartDate = new { min = items.Min(x => x.StartDate) }, EndDate = new { max = items.Max(x => x.EndDate) }, TDDirectCosts = new { sum = items.Sum(x => x.TDDirectCosts) }, TDDirectCosts_LM = new { sum = items.Sum(x => x.TDDirectCosts_LM) }, BUDirectCosts = new { sum = items.Sum(x => x.BUDirectCosts) }, BUDirectCosts_LM = new { sum = items.Sum(x => x.BUDirectCosts_LM) }, CostSavings = new { sum = items.Sum(x => x.CostSavings) } }; return result; } /// /// gets grouped items with "per group" aggregates. /// Currently calculates wrong aggreates with multiple grouping. Works fine with grouping by one column /// private IEnumerable GetGroups2(IEnumerable items, IEnumerable pageItems, IEnumerable groupInfos) { if (groupInfos == null) { yield break; } using (var enumerator = groupInfos.GetEnumerator()) { if (enumerator.MoveNext()) { bool isLast; do { var groupInfo = enumerator.Current; isLast = !enumerator.MoveNext(); var groups = items.GroupBy(groupInfo.Field, "it"); var aggregates = GetAggregates(groups, groupInfo); var pagedGroups = pageItems.GroupBy(groupInfo.Field, "it"); var tmp = from IGrouping pagedGroup in pagedGroups join IGrouping group1 in groups on pagedGroup.Key equals group1.Key join dynamic aggr in aggregates on pagedGroup.Key equals aggr.Key select new { aggregates = aggr.Aggregates, field = groupInfo.Field, value = pagedGroup.Key, items = isLast ? pagedGroup : GetGroups2(group1, pageItems, groupInfos.Skip(1)), hasSubgroups = !isLast }; foreach (var item in tmp) { yield return item; } } while (!isLast); } } } private IEnumerable GetAggregates(IEnumerable groups, Group groupInfo) { var aggrExpressions = groupInfo.Aggregates.Select(x => $"new ({TranslateAggregator(x)} as {x.Aggregate}) as {x.Field}"); var aggrExpressionsStr = string.Join(", ", aggrExpressions); var aggrSelector = $"new ({aggrExpressionsStr}) as Aggregates"; var selector = @"new (" + aggrSelector + ", it.Key)"; var aggreagates = groups.Select(selector); return aggreagates; } private string TranslateAggregator(Aggregator aggregator) { switch (aggregator.Aggregate) { case "count": return "it.Count()"; case "sum": return $"it.Sum(it.{aggregator.Field})"; case "min": return $"it.Min(it.{aggregator.Field})"; case "max": return $"it.Max(it.{aggregator.Field})"; case "avg": return $"it.Average(it.{aggregator.Field})"; } throw new InvalidOperationException($"Invalid aggregate '{aggregator.Aggregate}'"); } private string GetColor(decimal? performanceYellowThreshold, decimal? performanceRedThreshold, decimal? variationPercent) { //TODO: default project color looks like green performance color. So, use some color #CCCCCC if (variationPercent.HasValue && performanceYellowThreshold.HasValue && performanceRedThreshold.HasValue) { return variationPercent >= performanceRedThreshold.Value ? "#FF7777" : variationPercent < performanceRedThreshold.Value && variationPercent >= performanceYellowThreshold.Value ? "#FFFF77" : "#77FF77"; } return "#CCCCCC"; } /// /// Adds Scenario to the given Scenario Group /// private void CopyToGroup(Guid scenarioId, Guid groupId) { var scenarioIsExists = DbContext.Scenarios.Any(x => x.Id == scenarioId); if (scenarioIsExists) { DbContext.Scenario2Group.Add(new Scenario2Group { Id = Guid.NewGuid(), ScenarioId = scenarioId, GroupId = groupId }); DbContext.SaveChanges(); } } /// /// Removes Scenario from the given Scenario Group /// private void DeleteFromGroup(Guid scenarioId, Guid groupId) { var entity = DbContext.Scenario2Group.FirstOrDefault(x => x.ScenarioId == scenarioId && x.GroupId == groupId); if (entity != null) { DbContext.Scenario2Group.Remove(entity); DbContext.SaveChanges(); } } #endregion } }