using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Web; using EnVisage.Models; using System.Data.Entity.Infrastructure; using jQuery.DataTables.Mvc; using System.Data.SqlClient; using System.Data.Entity.Core.Objects; using System.Data; namespace EnVisage.Code.BLL { public class SkillsMatrixManager { public class MatrixFilterInternal { public List Teams = new List(); public List Resources = new List(); public List Skills = new List(); public List SkillGroups = new List(); public List Levels = new List(); public bool? IncludeInterested; public bool SkillWithDataOnly = false; } public SkillsMatrixManager(EnVisageEntities dbContext) { this.dbContext = dbContext; } protected EnVisageEntities dbContext = null; public void Save(SkillsMatrixSaveModel model) { if (model == null) throw new ArgumentNullException("model"); if (model.DatePoint < Utils.ConvertToUnixDate(Constants.UnixEpochDate)) throw new ArgumentException("model.DatePoint"); if (model.Values != null) { #region Saving levels DateTime datePointUtc = Utils.ConvertFromUnixDate(model.DatePoint); datePointUtc = datePointUtc.ToUniversalTime(); datePointUtc = new DateTime(datePointUtc.Year, datePointUtc.Month, datePointUtc.Day); List levelsToSave = model.Values.Where(k => (k.Value != null) && k.Value.LevelChanged).Select(x => x.Value).ToList(); foreach (SkillsMatrixSaveModel.DataItem item in levelsToSave) { Guid newId = Guid.NewGuid(); Skill2Resource rec = new Skill2Resource() { Id = newId, Type = (short)model.DataType, EffectiveDate = datePointUtc, ResourceId = item.ResourceId, SkillId = item.SkillId, Level = (short?)item.Level, Interested = item.Interested, DateCreated = DateTime.UtcNow }; dbContext.Skill2Resource.Add(rec); } #endregion #region Saving Skill Interests List interestsToSave = model.Values.Where(k => (k.Value != null) && k.Value.InterestChanged).Select(x => x.Value).ToList(); foreach (SkillsMatrixSaveModel.DataItem item in interestsToSave) { var recsToUpdate = dbContext.Skill2Resource.Where(x => x.SkillId.Equals(item.SkillId) && x.ResourceId.Equals(item.ResourceId)).ToList(); recsToUpdate.ForEach(x => x.Interested = item.Interested); } #endregion } } protected List GetPieChartData(SkillMatrixTimeLayer timeLayer, Skill2ResourceType dt, MatrixFilterInternal innerFilter) { var qry = GetSkillMatrixBaseDataQuery(timeLayer, dt); qry = ApplyFilter(qry, innerFilter); if (innerFilter.Levels.Count > 0) qry = qry.Where(x => innerFilter.Levels.Contains((int)x.Level)); if (innerFilter.IncludeInterested.HasValue) qry = qry.Where(x => x.Interested == innerFilter.IncludeInterested.Value); if (innerFilter.Skills.Count > 0) qry = qry.Where(x => innerFilter.Skills.Contains(x.SkillId)); if (innerFilter.SkillGroups.Count > 0) qry = qry.Where(x => innerFilter.SkillGroups.Contains(x.SkillGroupId)); return qry.ToList(); } public SkillsMatrixSaveModel LoadSkillMatrix(SkillMatrixTimeLayer timeLayer, Skill2ResourceType DataType, DateTime DatePoint, Guid userId, bool withChartData, SkillsMaxtrixFilterModel filter = null) { DateTime datePoint = DatePoint.ToUniversalTime().Date; SkillsMatrixSaveModel result = new SkillsMatrixSaveModel() { DataType = DataType, DatePoint = Utils.ConvertToUnixDate(datePoint) }; // Process incoming filter. Convert it to business-level filtering rules MatrixFilterInternal innerFilter = this.ProcessFilter(filter, userId); var prManager = new PeopleResourcesManager(dbContext); var teamResourceModels = prManager.LoadPeopleResourcesNonStrict(innerFilter.Teams, innerFilter.Resources, datePoint); var resourcesInMatrix = (teamResourceModels != null) ? teamResourceModels.Select(x => x.Id).ToList() : null; var qry = GetSkillMatrixBaseDataQuery(timeLayer, DataType); qry = ApplyFilter(qry, innerFilter); if (resourcesInMatrix != null) qry = qry.Where(x => resourcesInMatrix.Contains(x.ResourceId)); List skillsData = qry.ToList(); #region Fill Model with skills and skill groups List allSkills = null; if (innerFilter.SkillWithDataOnly) { // Retrive only skills with data var skillsWithData = skillsData.Select(x => x.SkillId).Distinct(); var skillGroupsWithData = skillsData.Select(x => x.SkillGroupId).Distinct(); allSkills = dbContext.Skills.AsNoTracking() .Where(x => skillsWithData.Contains(x.Id) || skillGroupsWithData.Contains(x.Id)) .Distinct().ToList(); } else { // Retrive all skills allSkills = dbContext.Skills.AsNoTracking().ToList(); } IEnumerable matrixSkillGroups = allSkills.Where(x => !x.ParentId.HasValue && ((innerFilter.SkillGroups.Count < 1) || innerFilter.SkillGroups.Contains(x.Id))).Select(x => x.Id); IEnumerable matrixSkills = allSkills.Where(x => (x.ParentId.HasValue || (!x.ParentId.HasValue && !x.HasChildren)) && ((innerFilter.Skills.Count < 1) || innerFilter.Skills.Contains(x.Id))) .Select(x => x.Id); result.SkillGroups = allSkills.Where(x => matrixSkillGroups.Contains(x.Id)) .Select(g => new SkillsMatrixSaveModel.SkillGroupDisplayModel() { Id = g.Id, Name = g.Name, Skills = allSkills.Where(s => ((s.ParentId.HasValue && s.ParentId.Value.Equals(g.Id)) || (s.Id.Equals(g.Id))) && matrixSkills.Contains(s.Id)).OrderBy(s => s.Name) .Select(s => new SkillsMatrixSaveModel.SkillDisplayModel() { // SA. If values assigned directly to Skill Group, the virtual Skill is created. // Its Id is the same, as its parent group has. Virtual skill doesn't has name, and has // attribute IsVirtual=true Id = s.Id, Name = (s.Id != g.Id) ? s.Name : String.Empty, SkillGroupId = g.Id, IsVirtual = (s.Id == g.Id) }).OrderBy(s => s.Name).ToList() }).OrderBy(g => g.Name).ToList(); #endregion if (withChartData) { const int RecordCount = 10; #region Bar Chart Data var BarData = new SkillsMatrixBarChartModel(); bool IncludeInterested = filter.IncludeInterested.HasValue && filter.IncludeInterested.Value; List GraphData = skillsData; if (innerFilter.Levels.Count > 0) { IEnumerable nullableLevels = innerFilter.Levels.Select(v => new int?(v)); GraphData = GraphData.Where(x => nullableLevels.Contains(x.Level)).ToList(); } if (innerFilter.IncludeInterested.HasValue) GraphData = GraphData.Where(x => x.Interested == innerFilter.IncludeInterested.Value).ToList(); if (innerFilter.Skills.Count > 0) GraphData = GraphData.Where(x => innerFilter.Skills.Contains(x.SkillId)).ToList(); if (innerFilter.SkillGroups.Count > 0) GraphData = GraphData.Where(x => innerFilter.SkillGroups.Contains(x.SkillGroupId)).ToList(); BarData.Colors = new string[] { "#eeeeee", "#deebf7", "#bdd7ee", "#9bc2e6", "#2f75b5" }; var skillTitles = new string[] { "Cannot Perform", "Knows All Elements", "Can Complete the Basics", "Proficient", "Exceptional" }; var sortedGroups = GraphData.GroupBy(r => r.SkillGroupId) .Select(g => new { id = g.Key, count = g.Count(), level0 = g.Where(p => p.Level == 0).Count(), level1 = g.Where(p => p.Level == 1).Count(), level2 = g.Where(p => p.Level == 2).Count(), level3 = g.Where(p => p.Level == 3).Count(), level4 = g.Where(p => p.Level == 4).Count() }) .OrderByDescending(l => l.count).Take(RecordCount).ToArray(); // Prepare data for graphs and diagramms var _GData = new List[5]; BarData.GTitles = new List>(); if (sortedGroups.Count() > 0) BarData.GMaxVal = sortedGroups[0].count; for (int i = 0; i < 5; i++) { _GData[i] = new List(); } var j = 1; foreach (var item in sortedGroups) { var title = new List(); title.Add(j + 0.25);//label width fix title.Add(result.SkillGroups.Where(x => x.Id == item.id).FirstOrDefault().Name); BarData.GTitles.Add(title); _GData[0].Add(new int[] { j, item.level0 }); _GData[1].Add(new int[] { j, item.level1 }); _GData[2].Add(new int[] { j, item.level2 }); _GData[3].Add(new int[] { j, item.level3 }); _GData[4].Add(new int[] { j, item.level4 }); j++; } var sortedTop = GraphData.GroupBy(r => r.SkillId).Select(g => new { id = g.Key, count = g.Count(), level0 = g.Where(p => p.Level == 0).Count(), level1 = g.Where(p => p.Level == 1).Count(), level2 = g.Where(p => p.Level == 2).Count(), level3 = g.Where(p => p.Level == 3).Count(), level4 = g.Where(p => p.Level == 4).Count() }) .OrderByDescending(l => l.count).Take(RecordCount).ToArray(); var _Data = new List[5]; if (sortedTop.Length > 0) BarData.MaxVal = sortedTop[0].count; BarData.Data = new List(); BarData.GroupData = new List(); BarData.Titles = new List>(); for (int i = 0; i < 5; i++) { _Data[i] = new List(); } j = 1; foreach (var item in sortedTop) { var title = new List(); title.Add(j + 0.25);//label width fix title.Add(allSkills.Where(x => x.Id == item.id).FirstOrDefault().Name); BarData.Titles.Add(title); _Data[0].Add(new int[] { j, item.level0 }); _Data[1].Add(new int[] { j, item.level1 }); _Data[2].Add(new int[] { j, item.level2 }); _Data[3].Add(new int[] { j, item.level3 }); _Data[4].Add(new int[] { j, item.level4 }); j++; } var z = _Data.Length; foreach (var item in _Data.Reverse()) { BarData.Data.Add(new { label = skillTitles[z - 1], data = item, color = BarData.Colors[z - 1], stack = true, bars = new { show = true, fill = 1, barWidth = 0.5, align = "center", lineWidth = 0 } }); z--; } z = _GData.Length; foreach (var item in _GData.Reverse()) { BarData.GroupData.Add(new { label = skillTitles[z - 1], data = item, color = BarData.Colors[z - 1], stack = true, bars = new { show = true, fill = 1, barWidth = 0.5, align = "center", lineWidth = 0 } }); z--; } result.BarGraphData = BarData; #endregion #region Pie Chart Data; var PieData = new SkillsMatrixPieChartModel(); List pastData = (timeLayer == SkillMatrixTimeLayer.Past) ? GraphData : GetPieChartData(SkillMatrixTimeLayer.Past, Skill2ResourceType.Actual, innerFilter); List presData = (timeLayer == SkillMatrixTimeLayer.Present) ? GraphData : GetPieChartData(SkillMatrixTimeLayer.Present, Skill2ResourceType.Actual, innerFilter); List futureData = (timeLayer == SkillMatrixTimeLayer.Future) ? GraphData : GetPieChartData(SkillMatrixTimeLayer.Future, Skill2ResourceType.Planned, innerFilter); PieData.PastData = GetMainToOtherDistribution(pastData, allSkills, false); PieData.PresentData = GetMainToOtherDistribution(presData, allSkills, false); PieData.FutureData = GetMainToOtherDistribution(futureData, allSkills, false); PieData.PastGroupedData = GetMainToOtherDistribution(pastData, allSkills, true); PieData.PresentGroupedData = GetMainToOtherDistribution(presData, allSkills, true); PieData.FutureGroupedData = GetMainToOtherDistribution(futureData, allSkills, true); result.PieGraphData = PieData; #endregion } #region Fill Model with Resources and Teams; // load all PeopleResource2Team relations, which were up to date at 'datePoint' date var allResources = new HashSet(); var allTeams = new HashSet(); // prepare model.Teams and model.Resources collections if (teamResourceModels != null) { foreach (var item in teamResourceModels) { // add resource to model.Resources collection if it does not exist there yet if (!allResources.Contains(item.Id)) { allResources.Add(item.Id); result.Resources.Add(new SkillsMatrixSaveModel.Resource { Id = item.Id, FirstName = item.FirstName, LastName = item.LastName, Teams = new List() }); } // add Team to model.Teams collection if it does not exist there yet if (!allTeams.Contains(item.Team.Id)) { allTeams.Add(item.Team.Id); result.Teams.Add(new SkillsMatrixSaveModel.Team { Id = item.Team.Id, Name = item.Team.Name, Resources = new List { item.Id } }); } else { // add resource to existing team record in model.Teams var teamItem = result.Teams.FirstOrDefault(t => t.Id == item.Team.Id); if (teamItem != null && teamItem.Resources.All(x => x != item.Id)) teamItem.Resources.Add(item.Id); } // add current team to all model.Resources records foreach (var resItem in result.Resources) { if (resItem.Id == item.Id) { resItem.Teams.Add(item.TeamId.Value); } } } } result.Teams = result.Teams.OrderBy(x => x.Name).ToList(); #endregion #region Get data items List dataValues = skillsData.Select(v => new SkillsMatrixSaveModel.DataItem() { ResourceId = v.ResourceId, SkillId = v.SkillId, Level = (int?)v.Level, Interested = v.Interested }).ToList(); #endregion #region Post-filtering result by Skill Levels and Interests bool filterByLevels = (innerFilter.Levels.Count > 0); bool filterByInterest = innerFilter.IncludeInterested.HasValue; if (filterByLevels || filterByInterest) { // Get resources and skills, suitable to SkillLevels and Interest filters List resourcesToDisplay = new List(); List skillsToDisplay = new List(); resourcesToDisplay = dataValues.Where(x => (!filterByLevels || (filterByLevels && x.Level.HasValue && innerFilter.Levels.Contains(x.Level.Value))) && (!filterByInterest || (filterByInterest && (x.Interested == innerFilter.IncludeInterested.Value)))) .Select(x => x.ResourceId).ToList(); skillsToDisplay = dataValues.Where(x => (!filterByLevels || (filterByLevels && x.Level.HasValue && innerFilter.Levels.Contains(x.Level.Value))) && (!filterByInterest || (filterByInterest && (x.Interested == innerFilter.IncludeInterested.Value)))) .Select(x => x.SkillId).ToList(); // Clear matrix data values, resource- and skill-lists dataValues.RemoveAll(x => !resourcesToDisplay.Contains(x.ResourceId) || !skillsToDisplay.Contains(x.SkillId)); result.SkillGroups.ForEach(sg => sg.Skills.RemoveAll(sk => !skillsToDisplay.Contains(sk.Id))); result.SkillGroups.RemoveAll(s => s.Skills.Count < 1); result.Resources.RemoveAll(r => !resourcesToDisplay.Contains(r.Id)); result.Teams.ForEach(t => t.Resources.RemoveAll(rId => !resourcesToDisplay.Contains(rId))); result.Teams.RemoveAll(t => t.Resources.Count < 1); } #endregion result.Values = dataValues .ToDictionary(k => k.ResourceId.ToString() + "#" + k.SkillId.ToString(), v => v); return result; } protected List GetMainToOtherDistribution(List GraphData, List skills, bool useSkillgroups) { var retData = new List(); var other = new List(); var total = 1.0M; var data = GraphData.GroupBy(r => (useSkillgroups ? r.SkillGroupId : r.SkillId)) .Select(g => new { id = g.Key, count = g.Sum(x=> x.Level + 1 ?? 0) }) .OrderByDescending(l => l.count).ToList(); var dataTotal = data.Select(x => x.count).Sum(); if (dataTotal > 0) foreach (var item in data) { var percentage = (decimal)item.count / dataTotal; if (total > .3M) { retData.Add(new SkillsMatrixPieChartPartModel() { TypeId = new List() { item.id }, Label = skills.Where(x => x.Id == item.id).FirstOrDefault().Name, Value = item.count }); total -= percentage; } else { other.Add(new SkillsMatrixPieChartPartModel() { TypeId = new List() { item.id }, Label = skills.Where(x => x.Id == item.id).FirstOrDefault().Name, Value = item.count }); } } if (other.Count() > 2) { retData.Add(new SkillsMatrixPieChartPartModel() { Label = "Other", TypeId = other.SelectMany(x => x.TypeId).ToList(), Value = other.Sum(x => x.Value) }); } else retData.AddRange(other); return retData; } protected IQueryable GetSkillMatrixBaseDataQuery(SkillMatrixTimeLayer timeLayer, Skill2ResourceType Type) { IQueryable qry = null; switch (timeLayer) { case SkillMatrixTimeLayer.Present: qry = dbContext.VW_Skill2Resource.AsNoTracking() .Where(x => (x.Type == (short)Skill2ResourceType.Actual) && (x.EffectiveDate <= DateTime.UtcNow)) .GroupBy(x => new { x.Type, x.ResourceId, x.SkillId }) .Select(x => x.OrderByDescending(a => a.EffectiveDate).ThenByDescending(b => b.DateCreated).Take(1)) .SelectMany(x => x); break; case SkillMatrixTimeLayer.Past: IQueryable presentRecords = dbContext.VW_Skill2Resource.AsNoTracking() .Where(x => (x.Type == (short)Skill2ResourceType.Actual) && (x.EffectiveDate <= DateTime.UtcNow)) .GroupBy(x => new { x.Type, x.ResourceId, x.SkillId }) .Select(x => x.OrderByDescending(a => a.EffectiveDate).ThenByDescending(b => b.DateCreated).Take(1)) .SelectMany(x => x) .Select(x => x.Id); qry = dbContext.VW_Skill2Resource.AsNoTracking() .Where(x => (x.Type == (short)Skill2ResourceType.Actual) && !presentRecords.Contains(x.Id)) .GroupBy(x => new { x.Type, x.ResourceId, x.SkillId }) .Select(x => x.OrderBy(a => a.PastYearDateOffset).Take(1)) .SelectMany(x => x); break; case SkillMatrixTimeLayer.Future: qry = dbContext.VW_Skill2Resource.AsNoTracking() .Where(x => (x.Type == (short)Skill2ResourceType.Planned)) .GroupBy(x => new { x.Type, x.ResourceId, x.SkillId }) .Select(x => x.OrderBy(a => a.FutureYearDateOffset).Take(1)) .SelectMany(x => x); break; } return qry; } protected IQueryable ApplyFilter(IQueryable baseQuery, MatrixFilterInternal filter) { if (baseQuery == null) throw new ArgumentNullException("baseQuery"); var qry = baseQuery; if (filter != null) { // Filtering by Resources if ((filter.Resources != null) && (filter.Resources.Count > 0)) qry = qry.Where(x => filter.Resources.Contains(x.ResourceId)); // Filtering by SkillGroups if ((filter.SkillGroups != null) && (filter.SkillGroups.Count > 0)) qry = qry.Where(x => filter.SkillGroups.Contains(x.SkillGroupId)); // Filtering by Skills (data may be assigned to Skill item and directly to Skill Group item) // In the las case, SkillId and SkillGroupId in the qry are equal if ((filter.Skills != null) && (filter.Skills.Count > 0)) qry = qry.Where(x => filter.Skills.Contains(x.SkillId) || filter.SkillGroups.Contains(x.SkillId)); } return qry; } protected MatrixFilterInternal ProcessFilter(SkillsMaxtrixFilterModel filter, Guid userId) { MatrixFilterInternal result = new MatrixFilterInternal(); if (filter != null) { #region Filter by Teams var teamMngr = new TeamManager(dbContext); List teamsFilterReviewd = (filter.Teams != null) && (filter.Teams.Count > 0) ? filter.Teams : null; List viewsFilterReviewd = (filter.Views != null) && (filter.Views.Count > 0) ? filter.Views : null; List companiesFilterReviewd = (filter.Companies != null) && (filter.Companies.Count > 0) ? filter.Companies : null; result.Teams = teamMngr.GetTeamsByUserFiltered(userId.ToString(), teamsFilterReviewd, viewsFilterReviewd, companiesFilterReviewd).Select(t=>t.TeamId).ToList(); #endregion #region Filter by Skills and Skill Groups List resultSkillGroups = new List(); List resultSkills = new List(); if ((filter.Skills != null) && (filter.Skills.Count > 0)) { // Add parent SkillGroups for Skills, listed in the incoming filter resultSkillGroups.AddRange(dbContext.Skills.AsNoTracking() .Where(x => x.ParentId.HasValue && filter.Skills.Contains(x.Id)) .Select(x => x.ParentId.Value)); // Add SkillGroups, which listed in the incoming Skill filter list resultSkillGroups.AddRange(dbContext.Skills.AsNoTracking() .Where(x => !x.ParentId.HasValue && filter.Skills.Contains(x.Id)) .Select(x => x.Id)); // Add child skills for selected Skill Groups resultSkills.AddRange(dbContext.Skills.AsNoTracking() .Where(x => x.ParentId.HasValue && filter.Skills.Contains(x.ParentId.Value)) .Select(x => x.Id)); // Include the only skills form filter.Skills (ignore groups here) resultSkills.AddRange(dbContext.Skills.AsNoTracking() .Where(x => x.ParentId.HasValue && filter.Skills.Contains(x.Id)) .Select(x => x.Id)); // Include Skills Groups with no child skills. Include as skills resultSkills.AddRange(dbContext.Skills.AsNoTracking() .Where(x => !x.ParentId.HasValue && filter.Skills.Contains(x.Id) && !x.HasChildren) .Select(x => x.Id)); } result.SkillGroups = resultSkillGroups.Distinct().ToList(); result.Skills = resultSkills.Distinct().ToList(); #endregion if (filter.Resources != null) result.Resources.AddRange(filter.Resources.Distinct()); if (filter.SkillLevels != null) result.Levels.AddRange(filter.SkillLevels.Distinct()); result.IncludeInterested = filter.IncludeInterested; result.Teams = result.Teams.Distinct().ToList(); result.SkillWithDataOnly = filter.SkillsWithDataOnly; } return result; } } }