EnVisageOnline/Main/Source/EnVisage/Code/BLL/SkillsMatrixManager.cs

623 lines
23 KiB
C#

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<Guid> Teams = new List<Guid>();
public List<Guid> Resources = new List<Guid>();
public List<Guid> Skills = new List<Guid>();
public List<Guid> SkillGroups = new List<Guid>();
public List<int> Levels = new List<int>();
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<SkillsMatrixSaveModel.DataItem> 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<SkillsMatrixSaveModel.DataItem> 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<VW_Skill2Resource> 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<VW_Skill2Resource> skillsData = qry.ToList();
#region Fill Model with skills and skill groups
List<Skill> 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<Guid> matrixSkillGroups = allSkills.Where(x => !x.ParentId.HasValue &&
((innerFilter.SkillGroups.Count < 1) || innerFilter.SkillGroups.Contains(x.Id))).Select(x => x.Id);
IEnumerable<Guid> 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<VW_Skill2Resource> GraphData = skillsData;
if (innerFilter.Levels.Count > 0)
{
IEnumerable<int?> 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<int[]>[5];
BarData.GTitles = new List<List<object>>();
if (sortedGroups.Count() > 0)
BarData.GMaxVal = sortedGroups[0].count;
for (int i = 0; i < 5; i++)
{
_GData[i] = new List<int[]>();
}
var j = 1;
foreach (var item in sortedGroups)
{
var title = new List<object>();
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<int[]>[5];
if (sortedTop.Length > 0)
BarData.MaxVal = sortedTop[0].count;
BarData.Data = new List<object>();
BarData.GroupData = new List<object>();
BarData.Titles = new List<List<object>>();
for (int i = 0; i < 5; i++)
{
_Data[i] = new List<int[]>();
}
j = 1;
foreach (var item in sortedTop)
{
var title = new List<object>();
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<VW_Skill2Resource> pastData = (timeLayer == SkillMatrixTimeLayer.Past) ? GraphData : GetPieChartData(SkillMatrixTimeLayer.Past, Skill2ResourceType.Actual, innerFilter);
List<VW_Skill2Resource> presData = (timeLayer == SkillMatrixTimeLayer.Present) ? GraphData : GetPieChartData(SkillMatrixTimeLayer.Present, Skill2ResourceType.Actual, innerFilter);
List<VW_Skill2Resource> 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<Guid>();
var allTeams = new HashSet<Guid>();
// 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<Guid>()
});
}
// 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<Guid> {
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<SkillsMatrixSaveModel.DataItem> 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<Guid> resourcesToDisplay = new List<Guid>();
List<Guid> skillsToDisplay = new List<Guid>();
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<SkillsMatrixPieChartPartModel> GetMainToOtherDistribution(List<VW_Skill2Resource> GraphData, List<Skill> skills, bool useSkillgroups)
{
var retData = new List<SkillsMatrixPieChartPartModel>();
var other = new List<SkillsMatrixPieChartPartModel>();
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<Guid>() { 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<Guid>() { 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<VW_Skill2Resource> GetSkillMatrixBaseDataQuery(SkillMatrixTimeLayer timeLayer,
Skill2ResourceType Type)
{
IQueryable<VW_Skill2Resource> 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<Guid> 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<VW_Skill2Resource> ApplyFilter(IQueryable<VW_Skill2Resource> 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<Guid> teamsFilterReviewd = (filter.Teams != null) && (filter.Teams.Count > 0) ?
filter.Teams : null;
List<Guid> viewsFilterReviewd = (filter.Views != null) && (filter.Views.Count > 0) ?
filter.Views : null;
List<Guid> 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<Guid> resultSkillGroups = new List<Guid>();
List<Guid> resultSkills = new List<Guid>();
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;
}
}
}