using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EnVisage.Code.Extensions; using EnVisage.Models; namespace EnVisage.Code.BLL { public class NonProjectTimeManager : ManagerBase { public NonProjectTimeManager(EnVisageEntities dbContext) : base(dbContext) { } public void DeleteNPT(Guid deleteNPTimeId, Guid? ResourceId) { var npTime = this.Load(deleteNPTimeId); var nptRas = this.DbContext.NonProjectTime2Resource.Where(x => x.NonProjectTimeId == npTime.Id && (!ResourceId.HasValue || x.PeopleResourceId == ResourceId.Value)).SelectMany(y => y.NonProjectTimeResourceAllocations).ToList(); this.DbContext.NonProjectTimeResourceAllocations.RemoveRange(nptRas); var nptTas = this.DbContext.NonProjectTime2Team.Where(x => x.NonProjectTimeId == deleteNPTimeId).SelectMany(u => u.NonProjectTimeTeamAllocations).ToList(); this.DbContext.NonProjectTimeTeamAllocations.RemoveRange(nptTas); this.DbContext.NonProjectTime2Resource.RemoveRange(this.DbContext.NonProjectTime2Resource.Where(x => x.NonProjectTimeId == deleteNPTimeId && (!ResourceId.HasValue || x.PeopleResourceId == ResourceId.Value)).ToList()); this.DbContext.NonProjectTime2Team.RemoveRange(this.DbContext.NonProjectTime2Team.Where(x => x.NonProjectTimeId == deleteNPTimeId).ToList()); if (!ResourceId.HasValue) this.DbContext.NonProjectTimes.RemoveRange(this.DbContext.NonProjectTimes.Where(x => x.Id == deleteNPTimeId).ToList()); if (this.IsContextLocal) DbContext.BulkSaveChanges(); } protected override NonProjectTime InitInstance() { return new NonProjectTime { Id = Guid.NewGuid() }; } protected override NonProjectTime RetrieveReadOnlyById(Guid key) { return DataTable.AsNoTracking().FirstOrDefault(t => t.Id == key); } public override DbSet DataTable { get { return DbContext.NonProjectTimes; } } public override NonProjectTime Save(NonProjectTimeModel model) { if (model == null) throw new ArgumentNullException(); if (model.IsTeamAssignmentMode && ((model.Teams == null) || (model.Teams.Count < 1))) throw new Exception("At least one team must be specified for Non-Project Time item"); if (!model.IsTeamAssignmentMode && ((model.Resources == null) || (model.Resources.Count < 1))) throw new Exception("At least one resource must be specified for Non-Project Time item"); if ((model.NonProjectTimeAllocations == null) || (model.NonProjectTimeAllocations.Count < 1)) throw new Exception("At least one allocation item must be specified for Non-Project Time"); #region Add Allocation Type if (model.NonProjectTimeCategoryId == Guid.Empty) { if (!SecurityManager.CheckSecurityObjectPermission(Areas.RD_ResourceNPTAllocationCategory, AccessLevel.Write)) { throw new InvalidOperationException("You haven't permissions to create a new Allocation Category"); } var npTimeCategory = new NonProjectTimeCategory { Id = Guid.NewGuid(), Name = model.NonProjectTimeCategoryName }; DbContext.NonProjectTimeCategories.Add(npTimeCategory); model.NonProjectTimeCategoryId = npTimeCategory.Id; } var npTime = base.Save(model); #endregion #region Delete old records var npTime2Resources = new List(); var npTime2Teams = new List(); var npTimeResourceAllocations = new List(); var npTimeTeamAllocations = new List(); var fcManager = new FiscalCalendarManager(DbContext); if (!Guid.Empty.Equals(model.Id)) { var nextWeek = model.NonProjectTimeEndDate.HasValue ? fcManager.GetFirstWeek(model.NonProjectTimeEndDate) : null; npTime2Resources = DbContext.NonProjectTime2Resource.Where(t => t.NonProjectTimeId == model.Id).ToList(); npTime2Teams = DbContext.NonProjectTime2Team.Where(t => t.NonProjectTimeId == model.Id).ToList(); var npTime2ResourcesIds = npTime2Resources.Select(x => x.Id).ToArray(); var npTime2TeamsIds = npTime2Teams.Select(x => x.Id).ToArray(); npTimeResourceAllocations = DbContext.NonProjectTimeResourceAllocations.Where(t => npTime2ResourcesIds.Contains(t.NonProjectTime2ResourceId)).ToList(); npTimeTeamAllocations = DbContext.NonProjectTimeTeamAllocations.Where(t => npTime2TeamsIds.Contains(t.NonProjectTime2TeamId)).ToList(); var npt2ResItemsToDelete = npTime2Resources.Where(x => (model.Resources == null) || model.Resources.All(s => s != x.PeopleResourceId)).ToArray(); var npt2TeamItemsToDelete = npTime2Teams.Where(x => (model.Teams == null) || model.Teams.All(s => s != x.TeamId)).ToArray(); var nptResAllocItemsToDelete = npTimeResourceAllocations.Where(x => x.WeekEndingDate < model.NonProjectTimeStartDate || ((nextWeek != null) && (x.WeekEndingDate > nextWeek.EndDate)) || npt2ResItemsToDelete.Any(s => x.NonProjectTime2ResourceId == s.Id)).ToArray(); var nptTeamAllocItemsToDelete = npTimeTeamAllocations.Where(x => x.WeekEndingDate < model.NonProjectTimeStartDate || ((nextWeek != null) && (x.WeekEndingDate > nextWeek.EndDate)) || npt2TeamItemsToDelete.Any(s => x.NonProjectTime2TeamId == s.Id)).ToArray(); DbContext.NonProjectTimeResourceAllocations.RemoveRange(nptResAllocItemsToDelete); DbContext.NonProjectTimeTeamAllocations.RemoveRange(nptTeamAllocItemsToDelete); DbContext.NonProjectTime2Resource.RemoveRange(npt2ResItemsToDelete); DbContext.NonProjectTime2Team.RemoveRange(npt2TeamItemsToDelete); } #endregion #region Prepare Allocations var allocations = new List(); if (!model.Permanent) { allocations = model.NonProjectTimeAllocations; } else { var allocation = model.NonProjectTimeAllocations.First(); var weeks = fcManager.GetFiscalCalendar(FiscalCalendarModel.FiscalYearType.Week, (model.EffectiveDateOfChange ?? model.NonProjectTimeStartDate), null, true, null, false); allocations = weeks.Select(x => new NonProjectTimeAllocationModel() { WeekStartDate = x.StartDate, WeekEndingDate = x.EndDate, HoursOff = allocation.HoursOff }).ToList(); } #endregion if (model.IsTeamAssignmentMode) { // Save NPT for teams #region Save Links to Resources var npt2TeamItemsNew = new Dictionary(); var nonProjectTime2TeamToAddCollection = new List(); for (var index = 0; index < model.Teams.Count; index++) { var npTime2Team = npTime2Teams.FirstOrDefault(x => x.TeamId == model.Teams[index]); if (npTime2Team == null) { npTime2Team = new NonProjectTime2Team() { Id = Guid.NewGuid(), NonProjectTimeId = npTime.Id, TeamId = model.Teams[index], Position = index + 1 }; nonProjectTime2TeamToAddCollection.Add(npTime2Team); } else { npTime2Team.Position = index + 1; } npt2TeamItemsNew.Add(npTime2Team.TeamId.ToString(), npTime2Team.Id); } DbContext.NonProjectTime2Team.AddRange(nonProjectTime2TeamToAddCollection); #endregion #region Save NonProjectTimeAllocations var prMngr = new PeopleResourcesManager(this.DbContext); var defaultUom = prMngr.GetDafaultUOM(); var teamsUomValues = (new TeamManager(DbContext)).GetTeamsUOM(model.Teams, model.NonProjectTimeStartDate); decimal currentUom; var nonProjectTimeTeamAllocationToAddCollection = new List(); foreach (var npTimeAllocation in allocations) { foreach (var team in model.Teams) { if (!teamsUomValues.ContainsKey(team)) { // UOM for team on current date point not found, probably because the team doesn't has // existing resources on the date point. Get default UOM currentUom = defaultUom; //throw new InvalidOperationException(string.Format("UoM was not found for the team {0}", team)); } else currentUom = teamsUomValues[team]; var hoursOff = Convert.ToInt32(Math.Min(npTimeAllocation.HoursOff, currentUom)); var npt2TeamItemId = npt2TeamItemsNew[team.ToString()]; var allocation = npTimeTeamAllocations.FirstOrDefault(x => x.NonProjectTime2TeamId == npt2TeamItemId && x.WeekEndingDate == npTimeAllocation.WeekEndingDate); if (allocation == null) { allocation = new NonProjectTimeTeamAllocation { Id = Guid.NewGuid(), NonProjectTime2TeamId = npt2TeamItemId, WeekEndingDate = npTimeAllocation.WeekEndingDate, HoursOff = hoursOff }; nonProjectTimeTeamAllocationToAddCollection.Add(allocation); } else { if (allocation.HoursOff != hoursOff) { allocation.HoursOff = hoursOff; } } } } DbContext.NonProjectTimeTeamAllocations.AddRange(nonProjectTimeTeamAllocationToAddCollection); #endregion } else { // Save NPT for resources #region Save Links to Resources var npt2ResItemsNew = new Dictionary(); var nonProjectTime2ResourceToAddCollection = new List(); for (var index = 0; index < model.Resources.Count; index++) { var npTime2Resource = npTime2Resources.FirstOrDefault(x => x.PeopleResourceId == model.Resources[index]); if (npTime2Resource == null) { npTime2Resource = new NonProjectTime2Resource() { Id = Guid.NewGuid(), NonProjectTimeId = npTime.Id, PeopleResourceId = model.Resources[index], Position = index + 1 }; nonProjectTime2ResourceToAddCollection.Add(npTime2Resource); } else { npTime2Resource.Position = index + 1; } npt2ResItemsNew.Add(npTime2Resource.PeopleResourceId.ToString(), npTime2Resource.Id); } DbContext.NonProjectTime2Resource.AddRange(nonProjectTime2ResourceToAddCollection); #endregion #region Save NonProjectTimeAllocations var resourcesUomValues = (new PeopleResourcesManager(DbContext)).GetResourcesUOM(model.Resources); var nonProjectTimeResourceAllocations = new List(); foreach (var npTimeAllocation in allocations) { foreach (var res in model.Resources) { if (!resourcesUomValues.ContainsKey(res)) throw new InvalidOperationException($"UoM was not found for the resource {res}"); var hoursOff = Convert.ToInt32(Math.Min(npTimeAllocation.HoursOff, resourcesUomValues[res])); var npt2ResItemId = npt2ResItemsNew[res.ToString()]; var allocation = npTimeResourceAllocations.FirstOrDefault(x => x.NonProjectTime2ResourceId == npt2ResItemId && x.WeekEndingDate == npTimeAllocation.WeekEndingDate); if (allocation == null) { allocation = new NonProjectTimeResourceAllocation { Id = Guid.NewGuid(), NonProjectTime2ResourceId = npt2ResItemId, WeekEndingDate = npTimeAllocation.WeekEndingDate, HoursOff = hoursOff }; nonProjectTimeResourceAllocations.Add(allocation); } else { if (allocation.HoursOff != hoursOff) { allocation.HoursOff = hoursOff; } } } } DbContext.NonProjectTimeResourceAllocations.AddRange(nonProjectTimeResourceAllocations); #endregion } return npTime; } /// /// Returns Non-Project Time item by it's ID. Teams and Resources are filtered by userId permissions /// public NonProjectTimeModel GetNonProjectTime(Guid npTimeId, string userId) { if (npTimeId.Equals(Guid.Empty)) return null; var dbRecord = DbContext.NonProjectTimes.Include(x => x.NonProjectTimeCategory).AsNoTracking().FirstOrDefault(x => x.Id.Equals(npTimeId)); if (dbRecord == null) throw new Exception($"Non-project Time item not found (Id: {npTimeId})"); var result = (NonProjectTimeModel)dbRecord; if (result.IsTeamAssignmentMode) result.Teams = this.GetTeams(npTimeId); else result.Resources = this.GetResources(npTimeId); // Get NPT allocations var fiscalWeeks = FiscalCalendarManager.GetFiscalCalendarWeeksByRange(result.NonProjectTimeStartDate, result.NonProjectTimeEndDate, this.DbContext) .ToDictionary(k => k.EndDate, v => v.StartDate); IEnumerable allocationsAsRaw; if (result.IsTeamAssignmentMode) { // Allocations to teams allocationsAsRaw = DbContext.NonProjectTimeTeamAllocations.Include(x => x.NonProjectTime2Team).AsNoTracking() .Where(x => result.Teams.Contains(x.NonProjectTime2Team.TeamId) && x.NonProjectTime2Team.NonProjectTimeId.Equals(npTimeId)) .OrderBy(x => x.WeekEndingDate).ToList() .Select(x => new NonProjectTimeAllocationModel() { WeekStartDate = fiscalWeeks[x.WeekEndingDate], WeekEndingDate = x.WeekEndingDate, HoursOff = x.HoursOff }); } else { // Allocations to resources allocationsAsRaw = DbContext.NonProjectTimeResourceAllocations.Include(x => x.NonProjectTime2Resource).AsNoTracking() .Where(x => result.Resources.Contains(x.NonProjectTime2Resource.PeopleResourceId) && x.NonProjectTime2Resource.NonProjectTimeId.Equals(npTimeId)) .OrderBy(x => x.WeekEndingDate).ToList() .Select(x => new NonProjectTimeAllocationModel() { WeekStartDate = fiscalWeeks[x.WeekEndingDate], WeekEndingDate = x.WeekEndingDate, HoursOff = x.HoursOff }); } if (allocationsAsRaw != null) { var allocationsGrouped = allocationsAsRaw.GroupBy(x => new { x.WeekStartDate, x.WeekEndingDate }, (key, grp) => new NonProjectTimeAllocationModel() { WeekStartDate = key.WeekStartDate, WeekEndingDate = key.WeekEndingDate, HoursOff = grp.Select(k => k.HoursOff).Min() }).ToList(); // Leave single allocation record for Permanent NPT var allocationsFiltered = result.Permanent ? allocationsGrouped.Where(x => x.WeekEndingDate >= DateTime.UtcNow.Date).Take(1).ToList() : allocationsGrouped; result.NonProjectTimeAllocations = allocationsFiltered; result.IsCurveConstant = result.NonProjectTimeAllocations.Select(x => x.HoursOff).Distinct().Count() == 1; } return result; } // Returns Non-Project time list as Dictionary[NptId, NptDataAsModel] public Dictionary GetNonProjectTimes(List npTimeIds, bool withTeamsAndResources) { if (npTimeIds == null) throw new ArgumentNullException("npTimeIds"); if (npTimeIds.Count < 1) return new Dictionary(); var qry = DbContext.NonProjectTimes.AsNoTracking().Where(x => npTimeIds.Contains(x.Id)); if (withTeamsAndResources) qry = qry.Include(x => x.NonProjectTime2Team).Include(x => x.NonProjectTime2Resource); var result = qry.ToList().Select(x => (NonProjectTimeModel)x) .GroupBy(x => x.Id).ToDictionary(k => k.Key, npt => npt.FirstOrDefault()); return result; } public List GetNonProjectTimeList(Guid resourceId, bool permanent, bool ishistory, bool includeInvalid) { return GetNonProjectTimeList(new List() { resourceId }, permanent, ishistory, includeInvalid); } public List GetNonProjectTimeList(List resources, bool permanent, bool ishistory, bool includeInvalid) { if (resources == null || resources.Count <= 0) return new List(); var uoms = (new PeopleResourcesManager(DbContext)).GetResourcesUOM(resources); var allocations = GetAllocations(resources); var weekEndings = allocations.SelectMany(x => x.Value).Select(x => x.WeekEndingDate).Distinct().ToList(); var fcWeekEndings = (new FiscalCalendarManager(DbContext)).GetFiscalCalendar(FiscalCalendarModel.FiscalYearType.Week, weekEndings, true, null, false) .ToDictionary(x => x.EndDate); var query = NonProjectTimeListItemModelBaseQuery.Where(x => allocations.Keys.Contains(x.Id) && x.Permanent == permanent); if (ishistory) query = query.Where(x => x.EndDate < DateTime.Today); else if (!permanent) query = query.Where(x => x.EndDate >= DateTime.Today); var npTimes = query.ToList(); foreach (var npTime in npTimes) { if (!allocations.ContainsKey(npTime.Id)) continue; var calculatedPercentsOff = new List(); foreach (var allocation in allocations[npTime.Id]) { if (!uoms.ContainsKey(allocation.PeopleResourceId)) continue; if (!fcWeekEndings.ContainsKey(allocation.WeekEndingDate)) continue; var distribution = CalculateWeekDistribution(fcWeekEndings[allocation.WeekEndingDate], npTime.StartDate, npTime.EndDate, uoms[allocation.PeopleResourceId]); if (distribution == null) continue; npTime.TotalUOMHours += distribution.HoursOff; npTime.TotalHours += allocation.HoursOff; if (uoms[allocation.PeopleResourceId] > 0) { var currentPercents = allocation.HoursOff / uoms[allocation.PeopleResourceId] * 100; if (npTime.TotalPercents == 0) { npTime.MinPercents = currentPercents; npTime.MaxPercents = npTime.MinPercents; } else { // Set flag, if final percents are npTime.ApproximatePercents = npTime.ApproximatePercents || (Math.Abs(npTime.TotalPercents - currentPercents) >= 1); if (npTime.MinPercents > currentPercents) npTime.MinPercents = currentPercents; if (npTime.MaxPercents < currentPercents) npTime.MaxPercents = currentPercents; } calculatedPercentsOff.Add(currentPercents); } npTime.TotalPercents = calculatedPercentsOff.Count > 0 ? calculatedPercentsOff.Average() : 0; } // for permanent resources we should take only one entri that is actual for current date var actualValue4Today = allocations[npTime.Id].FirstOrDefault(x => x.WeekEndingDate >= DateTime.UtcNow.Date); if (actualValue4Today != null) { npTime.ActualHours4Today = actualValue4Today.HoursOff; npTime.ActualPercents4Today = Convert.ToInt32(actualValue4Today.HoursOff / uoms[actualValue4Today.PeopleResourceId] * 100); } npTime.TotalPercents = Math.Round(npTime.TotalPercents); npTime.MinPercents = Math.Round(npTime.MinPercents > 0 ? (npTime.MinPercents < 100 ? npTime.MinPercents : 100) : 0); npTime.MaxPercents = Math.Round(npTime.MaxPercents > 0 ? (npTime.MaxPercents < 100 ? npTime.MaxPercents : 100) : 0); npTime.Details = npTime.Details.TruncateWithContinuation(60); } if (includeInvalid) { var alreadyExtractedNptItems = npTimes.Select(x => x.Id).ToList(); var invalidNptItemsQry = DbContext.VW_NonProjectTimeInvalidItems.AsNoTracking() .Where(x => resources.Contains(x.PeopleResourceId) && !alreadyExtractedNptItems.Contains(x.NonProjectTimeId)); if (ishistory) invalidNptItemsQry = invalidNptItemsQry.Where(x => x.EndDate.HasValue && (x.EndDate.Value < DateTime.Today)); else invalidNptItemsQry = invalidNptItemsQry.Where(x => x.EndDate.HasValue && (x.EndDate.Value >= DateTime.Today)); var invalidNptItemsAsModels = invalidNptItemsQry.ToList().Select(x => new NonProjectTimeListItemModel() { Id = x.NonProjectTimeId, Name = x.NonProjectTimeName, CategoryName = x.NonProjectTimeCategoryName, StartDate = x.StartDate, EndDate = x.EndDate, IsPercentMode = false, Details = x.Details.TruncateWithContinuation(60), Permanent = false, IsFromTeam = false, IsInvalid = true }).ToList(); npTimes.AddRange(invalidNptItemsAsModels); } return npTimes; } /// /// Returns NonProjectTime items for specified Team on a date point /// public List GetNonProjectTimes4Team(Guid teamId, bool permanent, bool ishistory, DateTime datePointUtc) { if (teamId == Guid.Empty) return new List(); DateTime dt = datePointUtc.ToUniversalTime().Date; FiscalCalendarManager fcMngr = new FiscalCalendarManager(DbContext); PeopleResourcesManager prMngr = new PeopleResourcesManager(DbContext); var teamResourcesModels = prMngr.LoadPeopleResourcesByTeamNonStrict(teamId, false, dt); var resources = teamResourcesModels.Select(x => x.Id).ToList(); var uoms = (new PeopleResourcesManager(DbContext)).GetResourcesUOM(resources); var allocations = GetAllocationsForTeam(teamId); var weekEndings = allocations.SelectMany(x => x.Value).Select(x => x.WeekEndingDate).Distinct().ToList(); var fcWeekEndings = fcMngr.GetFiscalCalendar(FiscalCalendarModel.FiscalYearType.Week, weekEndings, true, null, false) .ToDictionary(x => x.EndDate); var query = NonProjectTimeListItemModelBaseQuery; if (ishistory) query = query.Where(x => x.EndDate < dt); else { if (!permanent) query = query.Where(x => x.EndDate >= dt); } query = query.Where(x => allocations.Keys.Contains(x.Id) && x.Permanent == permanent); var npTimes = query.ToList(); // Remove NPT items, which have no allocations in current team on focused date range period List npTimeItemsToRemove = new List(); foreach (Guid npTimeId in allocations.Keys) { int allocItemsCount = 0; if (ishistory) // Historical NPT item should be not displayed, if it has no allocations in the past allocItemsCount = allocations[npTimeId] .Where(x => x.TeamId.Equals(teamId) && (x.WeekEndingDate < dt)).Count(); else // Future NPT item should be not displayed, if it has no allocations today and in future allocItemsCount = allocations[npTimeId] .Where(x => x.TeamId.Equals(teamId) && (x.WeekEndingDate >= dt)).Count(); if (allocItemsCount < 1) npTimeItemsToRemove.Add(npTimeId); } if (npTimeItemsToRemove.Count > 0) npTimes.RemoveAll(x => npTimeItemsToRemove.Contains(x.Id)); foreach (var npTime in npTimes) { if (!allocations.ContainsKey(npTime.Id)) continue; var calculatedPercentsOff = new List(); var npTimeAllocations = allocations[npTime.Id]; var maxUom = npTimeAllocations.Where(x => uoms.ContainsKey(x.PeopleResourceId)) .Select(x => uoms[x.PeopleResourceId]).Max(); foreach (var allocation in npTimeAllocations) { if (!uoms.ContainsKey(allocation.PeopleResourceId)) continue; if (!fcWeekEndings.ContainsKey(allocation.WeekEndingDate)) continue; var distribution = CalculateWeekDistribution(fcWeekEndings[allocation.WeekEndingDate], npTime.StartDate, npTime.EndDate, uoms[allocation.PeopleResourceId]); if (distribution == null) continue; npTime.TotalUOMHours += distribution.HoursOff; npTime.TotalHours += allocation.HoursOff; if (maxUom > 0) { var currentPercents = allocation.HoursOff / maxUom * 100; if (npTime.TotalPercents == 0) { npTime.MinPercents = currentPercents; npTime.MaxPercents = npTime.MinPercents; } else { // Set flag, if final percents are npTime.ApproximatePercents = npTime.ApproximatePercents || (Math.Abs(npTime.TotalPercents - currentPercents) >= 1); if (npTime.MinPercents > currentPercents) npTime.MinPercents = Math.Round(currentPercents); if (npTime.MaxPercents < currentPercents) npTime.MaxPercents = Math.Round(currentPercents); } calculatedPercentsOff.Add(currentPercents); } npTime.TotalPercents = calculatedPercentsOff.Count > 0 ? calculatedPercentsOff.Average() : 0; } // for permanent resources we should take only one entri that is actual for current date var actualValue4Today = allocations[npTime.Id].FirstOrDefault(x => x.WeekEndingDate >= DateTime.UtcNow.Date); if (actualValue4Today != null) { npTime.ActualHours4Today = actualValue4Today.HoursOff; if (uoms.Any(q => q.Key == actualValue4Today.PeopleResourceId)) { npTime.ActualPercents4Today = Convert.ToInt32(actualValue4Today.HoursOff / uoms[actualValue4Today.PeopleResourceId] * 100); } else { var defaultUom = (new PeopleResourcesManager(DbContext)).GetDafaultUOM(); npTime.ActualPercents4Today = Convert.ToInt32(actualValue4Today.HoursOff / defaultUom * 100); } } npTime.TotalPercents = Math.Round(npTime.TotalPercents); npTime.MinPercents = Math.Round(npTime.MinPercents > 0 ? (npTime.MinPercents < 100 ? npTime.MinPercents : 100) : 0); npTime.MaxPercents = Math.Round(npTime.MaxPercents > 0 ? (npTime.MaxPercents < 100 ? npTime.MaxPercents : 100) : 0); npTime.Details = npTime.Details.TruncateWithContinuation(60); } return npTimes; } public Dictionary> GetAllocations(Guid resourceId, Guid? npTimeId = null) { return GetAllocations(new List { resourceId }, npTimeId); } public Dictionary> GetAllocations(List resources, Guid? npTimeId = null) { var npTimeAllocationsQuery = DbContext.VW_NonProjectTimeAllocation.AsNoTracking() .Where(x => resources.Contains(x.PeopleResourceId)); if (npTimeId.HasValue) npTimeAllocationsQuery = npTimeAllocationsQuery.Where(x => x.NonProjectTimeId == npTimeId.Value); var npTimeAllocations = npTimeAllocationsQuery.ToList() .GroupBy(x => x.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.OrderBy(s => s.WeekEndingDate).ToList()); return npTimeAllocations; } public List GetResources(Guid npTimeId) { if (npTimeId == Guid.Empty) return new List(); var resources = DbContext.NonProjectTime2Resource.AsNoTracking() .Where(x => x.NonProjectTimeId == npTimeId) .OrderBy(x => x.Position) .Select(x => x.PeopleResourceId) .ToList(); return resources; } public List GetTeams(Guid npTimeId) { if (npTimeId == Guid.Empty) return new List(); var teams = DbContext.NonProjectTime2Team.AsNoTracking() .Where(x => x.NonProjectTimeId == npTimeId) .OrderBy(x => x.Position) .Select(x => x.TeamId) .ToList(); return teams; } public Dictionary RecalculateNPTimeAllocations(DateTime startDate, DateTime? endDate, List weeks, IEnumerable resourceIds, IEnumerable teamIds) { var result = new Dictionary(); var query = (from res in DbContext.VW_TeamResource.AsNoTracking() join ec in DbContext.ExpenditureCategory on res.ExpenditureCategoryId equals ec.Id join uom in DbContext.UOMs on ec.UOMId equals uom.Id select new { res.Id, res.TeamId, res.TeamStartDate, res.TeamEndDate, uom.UOMValue }).AsQueryable(); if (resourceIds != null && resourceIds.Any()) query = query.Where(t => resourceIds.Contains(t.Id)); if (teamIds != null && teamIds.Any()) query = query.Where(t => teamIds.Contains(t.TeamId)); var resources = query.Distinct().ToArray(); var firstWeek = weeks.FirstOrDefault() == null ? startDate : weeks.FirstOrDefault().EndDate; var lastWeek = weeks.LastOrDefault() == null ? endDate : weeks.LastOrDefault().EndDate; var allResHolidays = (new FiscalCalendarManager(DbContext)).GetHolidayAllocationsByResource(firstWeek, lastWeek, resourceIds: resources.Select(t => t.Id)); if (endDate.HasValue && startDate > endDate.Value) return result; if (weeks == null || weeks.Count <= 0) return result; foreach (var week in weeks) { // find maxumum resource UOM adjusted by holidays var uomValue = 0M; foreach (var resource in resources) { var holidayKoeff = allResHolidays.ContainsKey(resource.Id) && allResHolidays[resource.Id].ContainsKey(week.EndDate) ? allResHolidays[resource.Id][week.EndDate] : 1; uomValue = Math.Max(uomValue, resource.UOMValue * holidayKoeff); } var distribution = CalculateWeekDistribution(week, startDate, endDate, uomValue); if (distribution == null) continue; result.Add(Utils.ConvertToUnixDate(week.EndDate).ToString(), distribution); } return result; } #region NPT dates checks public NonProjectTimeDatesCheckResultModel CheckDateRangeViolationByResources(List resources, DateTime startDate, DateTime? endDate) { // Create dummy team, because we focused on check resource teams membership range, // and don't care of the teams, they are member of var result = new NonProjectTimeDatesCheckResultModel(); var dummyTeamItem = new NonProjectTimeDatesCheckResultModel.TeamCheckModel() { Id = Guid.Empty, Resources = new List() }; result.Teams.Add(dummyTeamItem); result.HasViolation = false; if ((resources == null) || (resources.Count < 1)) return result; var inactiveResources = DbContext.PeopleResources.Where(x => resources.Contains(x.Id) && !x.IsActiveEmployee).Select(x => x.Id).ToList(); var validationResults = new Dictionary(); // For every checkable resource we get the teams, he is member of with dates of membership var prMngr = new PeopleResourcesManager(this.DbContext); var resourcesWithTeams = prMngr.GetResourcesTeams(resources); // Get the summary teams membership date range, for every resource (through all resource's teams) List resourceExistingPeriod = resourcesWithTeams.Keys.Where(key => !inactiveResources.Contains(key) && (resourcesWithTeams[key].Count > 0)) .Select(key => new TeamMembershipModel { Id = key, StartDate = resourcesWithTeams[key].Select(x => x.StartDate).Min(), EndDate = resourcesWithTeams[key].Select(x => x.EndDate).Max(), }).ToList(); // Get resources, that are not member of any team var nonTeamResources = resources.Except(inactiveResources).Except(resourceExistingPeriod.Select(x => x.Id)).ToList(); nonTeamResources.ForEach(x => validationResults.Add(x, new NonProjectTimeDatesCheckResultModel.ResourceCheckModel { Id = x, ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NotMemberOfAnyTeam })); inactiveResources.ForEach(x => validationResults.Add(x, new NonProjectTimeDatesCheckResultModel.ResourceCheckModel { Id = x, ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.IsInactive })); // Check dates violation for reasources with teams foreach (var rmModel in resourceExistingPeriod) { bool skipNextChecks = false; var itemCheckResult = new NonProjectTimeDatesCheckResultModel.ResourceCheckModel(); itemCheckResult.Id = rmModel.Id; itemCheckResult.ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation; if (endDate.HasValue && (endDate.Value < rmModel.StartDate)) { // NPT is completelly in the past itemCheckResult.ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NptCompletlyBeforeResourceStartDate; skipNextChecks = true; } if (!skipNextChecks && rmModel.EndDate.HasValue && (startDate > rmModel.EndDate.Value)) { // NPT is completelly in future itemCheckResult.ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NptCompletlyAfterResourceEndDate; skipNextChecks = true; } if (!skipNextChecks && (startDate < rmModel.StartDate) && rmModel.EndDate.HasValue && endDate.HasValue && (endDate.Value > rmModel.EndDate.Value)) { // NPT is out of both resource's dates itemCheckResult.ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NptOutBothResourceDates; skipNextChecks = true; } if (!skipNextChecks && (startDate < rmModel.StartDate)) { // NPT is partially in the past itemCheckResult.ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyBeforeResourceStartDate; skipNextChecks = true; } if (!skipNextChecks && rmModel.EndDate.HasValue && (startDate < rmModel.EndDate.Value) && endDate.HasValue && (endDate.Value > rmModel.EndDate.Value)) { // Now for permanent NPT's we don't register a violation, if a resource has limited End Date // (not permanent), though NPT continues after resource finish date. // To change the logic to treat this situation as violation, set the condition for this 'if' as // follows: // if (!skipNextChecks && rmModel.EndDate.HasValue && (startDate < rmModel.EndDate.Value) && // (!endDate.HasValue || (endDate.HasValue && (endDate.Value > rmModel.EndDate.Value))) itemCheckResult.ViolationType = NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyAfterResourceEndDate; } if (itemCheckResult.ViolationType != NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation) validationResults.Add(itemCheckResult.Id, itemCheckResult); } if (validationResults.Count > 0) { // Add resource attributes and sorting var vialotedResourceIds = validationResults.Keys.ToList(); dummyTeamItem.Resources = DbContext.PeopleResources.AsNoTracking().Where(x => vialotedResourceIds.Contains(x.Id)) .OrderBy(x => x.LastName).ThenBy(x => x.FirstName).ToList() .Select(x => new NonProjectTimeDatesCheckResultModel.ResourceCheckModel() { Id = x.Id, Name = String.Format("{0} {1}", x.FirstName, x.LastName), ViolationType = validationResults[x.Id].ViolationType }).ToList(); result.HasViolation = validationResults.Values.Any(x => x.ViolationType != NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation); } return result; } public class TeamCheckModel { public Guid Id; public string Name; public DateTime StartDate; public DateTime? EndDate; public NonProjectTimeDatesCheckResultModel.ViolationType Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation; } public class ResourceCheckModel { public Guid Id; public string FirstName; public string LastName; public Dictionary Teams; public NonProjectTimeDatesCheckResultModel.ViolationType Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation; } public NonProjectTimeDatesCheckResultModel CheckDateRangeViolationByTeams(List teams, DateTime startDate, DateTime? endDate) { // Idea: // For each resource we check the consequence of his teams (team membership records) to fit the dates // of incoming NPT. We check the only team records for incoming teams list. And throw out all other // resource's team membership records, that are not specified in incoming teams list. // If all the team records of a resource, we found in DB, are completelly out of NPT dates, we // treat this situation as OK. It meant absent of any violation. So, the NPT doesn't touch this resource. // We avoid checks for inactive resources also. // We perform checks for the only resources, who has at least one team membership record with dates, // that are are completelly or partially 'covered' with NPT dates period. // // First, we get a complete list of active resources for incoming teams list. To perform checks well, // we must take a look to every resource as a consequent list of his teams. To get this point of view // we must turn inside out data, we got from TeamManager. We make a struct, that is a plain list of resource, // where every resource contains a list of his team membership records. // Then for every resource we check every team membership record dates againt NPT dates. // Browsing of all possible variants of the check results, we got some rules for identification of // different type of violations. // 1. If all team membership records for a resource got the result 'NptOutBothResourceDates', the NPT seems to // begin before the resource started in his teams, and finished after, the resource finished in all his teams. // 2. If we got at least one team membership record with the result 'NptPartiallyBeforeResourceStartDate' and at the same // time got at least one team membership record with the result 'NptPartiallyAfterResourceStartDate', the // resource doesn't have any violation. The case seems to be the situation, when NPT started, when the resource // was a member of one team, and finished, when the resource was a member of some other team. But NPT completelly // 'covers' resource existing dates. So, it's ok. // 3. If we got at least one team membership records with the result 'NptPartiallyBeforeResourceStartDate', the // violation take place, because NPT started before the resource started in all his teams (from incoming teams list). // 4. If we got at least one team membership records with the result 'NptPartiallyAfterResourceStartDate', the // violation take place, because NPT finished after the resource finished in all his teams (from incoming teams list). // 5. If we got any other variants of team membership records check results. It seems the resource has no violation, and // it is good. // Get resources for teams var teamMngr = new TeamManager(DbContext); var teamsWithResources = teamMngr.LoadTeamsWithResourcesByUser(null, teams); // Turn data inside out: from resources to his teams var checkTempResults = new Dictionary(); foreach (var team in teamsWithResources) { if ((team.PeopleResources != null) && (team.PeopleResources.Count > 0)) { foreach (var resource in team.PeopleResources) { if (!resource.IsActiveEmployee) // Exclude inactive resources from check continue; if (!checkTempResults.ContainsKey(resource.Id)) { var resourceCheckModel = new ResourceCheckModel() { Id = resource.Id, FirstName = resource.FirstName, LastName = resource.LastName, Teams = new Dictionary() }; checkTempResults.Add(resource.Id, resourceCheckModel); } if (!checkTempResults[resource.Id].Teams.ContainsKey(team.Id)) { var teamCheckModel = new TeamCheckModel() { Id = team.Id, Name = team.Name, StartDate = resource.StartDate, EndDate = resource.EndDate }; checkTempResults[resource.Id].Teams.Add(team.Id, teamCheckModel); } } } } // Check resource's team membership records and register violations foreach (var resource in checkTempResults.Values) { if ((resource.Teams != null) && (resource.Teams.Count > 0)) { foreach (var team in resource.Teams.Values) { bool skipNextChecks = false; var itemCheckResult = NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation; if (endDate.HasValue && (endDate.Value < team.StartDate)) { // NPT is completelly in the past itemCheckResult = NonProjectTimeDatesCheckResultModel.ViolationType.NptCompletlyBeforeResourceStartDate; skipNextChecks = true; } if (!skipNextChecks && team.EndDate.HasValue && (startDate > team.EndDate.Value)) { // NPT is completelly in future itemCheckResult = NonProjectTimeDatesCheckResultModel.ViolationType.NptCompletlyAfterResourceEndDate; skipNextChecks = true; } if (!skipNextChecks && (startDate < team.StartDate) && team.EndDate.HasValue && endDate.HasValue && (endDate.Value > team.EndDate.Value)) { // NPT is out of both resource's dates itemCheckResult = NonProjectTimeDatesCheckResultModel.ViolationType.NptOutBothResourceDates; skipNextChecks = true; } if (!skipNextChecks && (startDate < team.StartDate)) { // NPT is partially in the past itemCheckResult = NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyBeforeResourceStartDate; skipNextChecks = true; } if (!skipNextChecks && team.EndDate.HasValue && (startDate < team.EndDate.Value) && endDate.HasValue && (endDate.Value > team.EndDate.Value)) { itemCheckResult = NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyAfterResourceEndDate; } team.Violation = itemCheckResult; } } } // Analyze in complex the results for team membership records of every resource foreach (var resource in checkTempResults.Values) { if ((resource.Teams != null) && (resource.Teams.Count > 0)) { var teamCheckModels = resource.Teams.Values.ToList(); bool skipNextChecks = false; if (teamCheckModels.TrueForAll(x => x.Violation == NonProjectTimeDatesCheckResultModel.ViolationType.NptOutBothResourceDates)) { // Rule 1 from the list above resource.Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NptOutBothResourceDates; skipNextChecks = true; } if (!skipNextChecks && teamCheckModels.Any(x => x.Violation == NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyBeforeResourceStartDate) && teamCheckModels.Any(x => x.Violation == NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyAfterResourceEndDate)) { // Rule 2 from the list above resource.Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation; skipNextChecks = true; } if (!skipNextChecks && teamCheckModels.Any(x => x.Violation == NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyBeforeResourceStartDate)) { // Rule 3 from the list above resource.Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyBeforeResourceStartDate; skipNextChecks = true; } if (!skipNextChecks && teamCheckModels.Any(x => x.Violation == NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyAfterResourceEndDate)) { // Rule 4 from the list above resource.Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NptPartiallyAfterResourceEndDate; skipNextChecks = true; } if (!skipNextChecks) { // Rule 5 from the list above resource.Violation = NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation; } } } // Remove resources with no violation var resourceRecordsToRemove = new List(); foreach (var key in checkTempResults.Keys) { if (checkTempResults[key].Violation == NonProjectTimeDatesCheckResultModel.ViolationType.NoViolation) resourceRecordsToRemove.Add(key); } foreach (var key in resourceRecordsToRemove) checkTempResults.Remove(key); // Format data to resulted structs NonProjectTimeDatesCheckResultModel result = new NonProjectTimeDatesCheckResultModel(); NonProjectTimeDatesCheckResultModel.TeamCheckModel dummyTeamItem = new NonProjectTimeDatesCheckResultModel.TeamCheckModel() { Id = Guid.Empty, Resources = new List() }; result.Teams.Add(dummyTeamItem); result.HasViolation = false; dummyTeamItem.Resources = checkTempResults.Values.OrderBy(x => x.LastName).ThenBy(x => x.FirstName).Select(r => new NonProjectTimeDatesCheckResultModel.ResourceCheckModel() { Id = r.Id, Name = $"{r.FirstName} {r.LastName}", ViolationType = r.Violation }).ToList(); result.HasViolation = (dummyTeamItem.Resources.Count > 0); return result; } #endregion #region DAL methods private IQueryable BuildBasicNonProjectTimeAllocationsQuery(IEnumerable resources = null, IEnumerable teamIds = null, List npTimeIds = null, DateTime? startDate = null, DateTime? endDate = null) { var npTimesAllocationQuery = DbContext.VW_NonProjectTimeAllocation.AsNoTracking().AsQueryable(); if (resources != null && resources.Any()) npTimesAllocationQuery = npTimesAllocationQuery.Where(x => resources.Contains(x.PeopleResourceId)); if (teamIds != null && teamIds.Any()) npTimesAllocationQuery = npTimesAllocationQuery.Where(x => teamIds.Contains(x.TeamId)); if (npTimeIds != null && npTimeIds.Count > 0) npTimesAllocationQuery = npTimesAllocationQuery.Where(x => npTimeIds.Contains(x.NonProjectTimeId)); if (startDate.HasValue) npTimesAllocationQuery = npTimesAllocationQuery.Where(x => x.WeekEndingDate >= startDate.Value); if (endDate.HasValue) npTimesAllocationQuery = npTimesAllocationQuery.Where(x => x.WeekEndingDate <= endDate.Value); return npTimesAllocationQuery; } public Dictionary> GetNPTResourceAllocationsByTeam(List nptIds, Guid teamId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(teamId)) return new Dictionary>(); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeResourceAllocations join npt2res in DbContext.NonProjectTime2Resource on alloc.NonProjectTime2ResourceId equals npt2res.Id join npt in DbContext.NonProjectTimes on npt2res.NonProjectTimeId equals npt.Id join res in DbContext.VW_TeamResource on npt2res.PeopleResourceId equals res.Id where nptIds.Contains(npt.Id) && teamId == res.TeamId select alloc).Include(t => t.NonProjectTime2Resource).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Resource.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } public Dictionary> GetNPTResourceAllocationsByView(List nptIds, Guid viewId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(viewId)) return new Dictionary>(); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeResourceAllocations join npt2res in DbContext.NonProjectTime2Resource on alloc.NonProjectTime2ResourceId equals npt2res.Id join npt in DbContext.NonProjectTimes on npt2res.NonProjectTimeId equals npt.Id join res in DbContext.VW_TeamResource on npt2res.PeopleResourceId equals res.Id join t2v in DbContext.VW_Team2View on res.TeamId equals t2v.TeamId where nptIds.Contains(npt.Id) && viewId == t2v.ViewId select alloc).Include(t => t.NonProjectTime2Resource).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Resource.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } public Dictionary> GetNPTResourceAllocationsByCompany(List nptIds, Guid companyId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(companyId)) return new Dictionary>(); var companyIds = DbContext.Companies.Where(c => c.ParentCompanyId == companyId).Select(c => c.Id).ToList(); companyIds.Add(companyId); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeResourceAllocations join npt2res in DbContext.NonProjectTime2Resource on alloc.NonProjectTime2ResourceId equals npt2res.Id join npt in DbContext.NonProjectTimes on npt2res.NonProjectTimeId equals npt.Id join res in DbContext.VW_TeamResource on npt2res.PeopleResourceId equals res.Id join t in DbContext.Teams on res.TeamId equals t.Id where nptIds.Contains(npt.Id) && t.CompanyId.HasValue && companyIds.Contains(t.CompanyId.Value) select alloc).Include(t => t.NonProjectTime2Resource).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Resource.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } public Dictionary> GetNPTResourceAllocations(List nptIds, Guid resourceId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(resourceId)) return new Dictionary>(); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeResourceAllocations join npt2res in DbContext.NonProjectTime2Resource on alloc.NonProjectTime2ResourceId equals npt2res.Id join npt in DbContext.NonProjectTimes on npt2res.NonProjectTimeId equals npt.Id where nptIds.Contains(npt.Id) && resourceId == npt2res.PeopleResourceId select alloc).Include(t => t.NonProjectTime2Resource).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Resource.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } public Dictionary> GetNPTTeamAllocationsByTeam(List nptIds, Guid teamId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(teamId)) return new Dictionary>(); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeTeamAllocations join npt2t in DbContext.NonProjectTime2Team on alloc.NonProjectTime2TeamId equals npt2t.Id join npt in DbContext.NonProjectTimes on npt2t.NonProjectTimeId equals npt.Id where nptIds.Contains(npt.Id) && teamId == npt2t.TeamId select alloc).Include(t => t.NonProjectTime2Team).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Team.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } public Dictionary> GetNPTTeamAllocationsByView(List nptIds, Guid viewId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(viewId)) return new Dictionary>(); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeTeamAllocations join npt2t in DbContext.NonProjectTime2Team on alloc.NonProjectTime2TeamId equals npt2t.Id join npt in DbContext.NonProjectTimes on npt2t.NonProjectTimeId equals npt.Id join t2v in DbContext.VW_Team2View on npt2t.TeamId equals t2v.TeamId where nptIds.Contains(npt.Id) && viewId == t2v.ViewId select alloc).Include(t => t.NonProjectTime2Team).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Team.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } public Dictionary> GetNPTTeamAllocationsByCompany(List nptIds, Guid companyId) { if (nptIds == null || nptIds.Count == 0 || Guid.Empty.Equals(companyId)) return new Dictionary>(); var companyIds = DbContext.Companies.Where(c => c.ParentCompanyId == companyId).Select(c => c.Id).ToList(); companyIds.Add(companyId); var npTimeAllocationsQuery = (from alloc in DbContext.NonProjectTimeTeamAllocations join npt2t in DbContext.NonProjectTime2Team on alloc.NonProjectTime2TeamId equals npt2t.Id join npt in DbContext.NonProjectTimes on npt2t.NonProjectTimeId equals npt.Id join t in DbContext.Teams on npt2t.TeamId equals t.Id where nptIds.Contains(npt.Id) && t.CompanyId.HasValue && companyIds.Contains(t.CompanyId.Value) select alloc).Include(t => t.NonProjectTime2Team).Distinct(); var npTimeAllocations = npTimeAllocationsQuery.ToArray() .GroupBy(t => t.NonProjectTime2Team.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.ToList()); return npTimeAllocations; } /// /// Returns UOMs for each team in the specified NPT-list. UOM for team is the Max UOM of all it's resources /// /// Team level NPTs are processed only public Dictionary GetNPTUOMs(List nptIds) { if (nptIds == null || nptIds.Count < 1) return new Dictionary(); var teamUoms = (from npt in DbContext.NonProjectTime2Team.AsNoTracking() join p in DbContext.VW_TeamResource.AsNoTracking() on npt.TeamId equals p.TeamId join ec in DbContext.ExpenditureCategory.AsNoTracking() on p.ExpenditureCategoryId equals ec.Id join uom in DbContext.UOMs.AsNoTracking() on ec.UOMId equals uom.Id where nptIds.Contains(npt.NonProjectTimeId) && npt.NonProjectTime.IsTeamMode select new { NonProjectTimeId = npt.NonProjectTimeId, UOMValue = uom.UOMValue }).Distinct().ToArray(); return teamUoms.GroupBy(x => x.NonProjectTimeId, (key, grp) => new { NonProjectTimeId = key, UOMValue = grp.Select(j => j.UOMValue).Max() }).ToDictionary(x => x.NonProjectTimeId, x => x.UOMValue); } #endregion #region BLL methods /// /// Returns all NonProjectTime allocations for specified team and NPT. Key is NPT /// /// An Id of the target team which NPT allocations we're going to receive. /// An Id of the NPT record which allocations we're going to receive. public Dictionary> GetAllocationsForTeam(Guid teamId, Guid? npTimeId = null, DateTime? startDate = null, DateTime? endDate = null) { if (npTimeId.HasValue) return GetAllocationsForTeam(new List { teamId }, new List { npTimeId.Value }, startDate, endDate); else return GetAllocationsForTeam(new List { teamId }, null, startDate, endDate); } /// /// Returns all NonProjectTime allocations for specified team and NPT. Key is NPT /// /// A list of team Ids which NPT allocations we're going to receive. /// A list of NPT record Ids which allocations we're going to receive. public Dictionary> GetAllocationsForTeam(List teamIds, List npTimeIds = null, DateTime? startDate = null, DateTime? endDate = null) { if ((teamIds == null || teamIds.Count == 0) && (npTimeIds == null || npTimeIds.Count == 0)) throw new ArgumentNullException("teamIds", "Either teamIds or npTimeIds are requried"); var npTimeAllocationsQuery = BuildBasicNonProjectTimeAllocationsQuery(teamIds: teamIds, startDate: startDate, endDate: endDate); var npTimeAllocations = npTimeAllocationsQuery.ToList() .GroupBy(x => x.NonProjectTimeId) .ToDictionary(x => x.Key, x => x.OrderBy(s => s.WeekEndingDate).ToList()); return npTimeAllocations; } /// /// Returns total non-project times grouped by resource and then by non-project time category and then by week for the specified period and the specified set of resource Ids. /// public Dictionary> GetNonProjectTimes4Teams(IEnumerable teams, DateTime? startDate = null, DateTime? endDate = null) { var resourceAllocations = GetNPTAllocation4Team(teams, startDate, endDate) .GroupBy(t => t.PeopleResourceId) .ToDictionary(k => k.Key, v => v.ToArray()); var result = new Dictionary>(); foreach (var resItem in resourceAllocations) { // init resource level dictionary if (!result.Keys.Contains(resItem.Key)) result.Add(resItem.Key, new Dictionary()); var resourceResult = result[resItem.Key]; // iterate through allocations specific for current resource grouped by NPT Category foreach (var catItem in resItem.Value.GroupBy(k => new { NonProjectTimeCategoryId = k.NonProjectTimeCategoryId, k.NonProjectTimeCategoryName, NonProjectTimeId = k.NonProjectTimeId, k.NonProjectTimeName, k.IsFromTeam, k.ExpenditureCategoryId })) { // init NPT Category level dictionary if (!resourceResult.Keys.Contains(catItem.Key.NonProjectTimeId.ToString())) resourceResult.Add(catItem.Key.NonProjectTimeId.ToString(), new ResourceNptModel { Id = catItem.Key.NonProjectTimeId, Name = catItem.Key.NonProjectTimeName, isTeamWide = catItem.Key.IsFromTeam, CategoryId = catItem.Key.NonProjectTimeCategoryId, ExpenditureCategoryId = catItem.Key.ExpenditureCategoryId, Allocations = catItem.GroupBy(k => k.WeekEndingDate) .ToDictionary(v => Utils.ConvertToUnixDate(v.Key).ToString(), val => Convert.ToDecimal(val.Sum(s => s.HoursOff))), Costs = catItem.GroupBy(k => k.WeekEndingDate) .ToDictionary(v => Utils.ConvertToUnixDate(v.Key).ToString(), val => Convert.ToDecimal(val.Sum(s => s.Cost))) }); } } return result; } public NonProjectTimeSummaryItem[] GetNPTAllocation4Team(IEnumerable teamIds, DateTime? startDate = null, DateTime? endDate = null) { // To-Do: test me! var npTimesAllocationQuery = BuildBasicNonProjectTimeAllocationsQuery(teamIds: teamIds, startDate: startDate, endDate: endDate); return ConvertNptQuery2Array(npTimesAllocationQuery); } public NonProjectTimeSummaryItem[] GetNonProjectTimesByResources(IEnumerable resources, DateTime? startDate = null, DateTime? endDate = null) { var npTimesAllocationQuery = BuildBasicNonProjectTimeAllocationsQuery(resources: resources, startDate: startDate, endDate: endDate); return ConvertNptQuery2Array(npTimesAllocationQuery); } public void UpdateNptAllocationsOnResourceExpCatChange(Guid resourceId, Guid newExpCatId) { if (resourceId.Equals(Guid.Empty)) throw new ArgumentNullException(nameof(resourceId)); if (newExpCatId.Equals(Guid.Empty)) throw new ArgumentNullException(nameof(newExpCatId)); // Get Unit of measure for new EC to limit hours-off for npt allocations (make them not greater, than new EC uom) var ecMngr = new ExpenditureCategoryManager(DbContext); var newExpCat = ecMngr.GetExpenditureDetails(newExpCatId); if (newExpCat == null) throw new ArgumentException($"There is no EC with Id = {newExpCatId}"); // Perform checks and update for NPTs var npt2ResourceRecords = DbContext.NonProjectTime2Resource .Where(x => x.PeopleResourceId.Equals(resourceId)) .Include(x => x.NonProjectTimeResourceAllocations).ToArray(); foreach (var resourceRecord in npt2ResourceRecords) { if (resourceRecord.NonProjectTimeResourceAllocations == null) continue; foreach (var allocation in resourceRecord.NonProjectTimeResourceAllocations) if (allocation.HoursOff > newExpCat.UOMValue) allocation.HoursOff = (int)newExpCat.UOMValue; } } #endregion #region Private Members private IQueryable NonProjectTimeListItemModelBaseQuery { get { var query = DbContext.NonProjectTimes.AsNoTracking() .Select(x => new NonProjectTimeListItemModel { Id = x.Id, Name = x.Name, CategoryName = x.NonProjectTimeCategory.Name, StartDate = x.StartDate, EndDate = x.EndDate, Cost = x.Cost, Details = x.Details, IsPercentMode = x.IsPercentsMode, Permanent = x.Permanent, IsFromTeam = x.IsTeamMode }); return query; } } private IQueryable NonProjectTimeModelBaseQuery { get { var query = DbContext.NonProjectTimes.AsNoTracking().Include(t => t.NonProjectTimeCategory) .Select(x => new NonProjectTimeModel { Id = x.Id, NonProjectTimeName = x.Name, NonProjectTimeCategoryId = x.NonProjectTimeCategoryId, NonProjectTimeCategoryName = x.NonProjectTimeCategory.Name, NonProjectTimeStartDate = x.StartDate, NonProjectTimeEndDate = x.EndDate, NonProjectTimeCost = x.Cost, Permanent = x.Permanent, Details = x.Details, IsPercentsMode = x.IsPercentsMode, IsTeamAssignmentMode = x.IsTeamMode, IsHistory = (x.EndDate.HasValue && x.EndDate.Value < DateTime.Today) }); return query; } } private NonProjectTimeDistributionModel CalculateWeekDistribution(FiscalCalendar week, DateTime startDate, DateTime? endDate, decimal uomValue) { if (week == null) throw new ArgumentNullException("week"); var fcWeekLength = (week.EndDate - week.StartDate).Days + 1; if (fcWeekLength <= 0) throw new InvalidOperationException(string.Format("Cannot calculate NP Time distribution because of incorrect week range in the fiscal calendar: {0:MM/dd/yyyy} - {1:MM/dd/yyyy}", week.StartDate, week.EndDate)); var npWeekLength = fcWeekLength; if (week.StartDate <= startDate && week.EndDate >= startDate) { if (endDate.HasValue && week.StartDate <= endDate.Value && week.EndDate >= endDate.Value) npWeekLength = (endDate.Value - startDate).Days + 1; // if full range is in the middle of the week else npWeekLength = (week.EndDate - startDate).Days + 1; // if start date is in the middle of the week } else if (endDate.HasValue) { // e.g. we have the following week endings in the fiscal calendar: 2016-12-30, 2017-01-13, 2017-01-06 does not exist // user types end date for NP Time to 2016-12-31 and we show him week from 2017-01-07 to 2017-01-13 // and until user's end date does not cover any day from the existing week we should show this week with zero allocation if (endDate.Value < week.StartDate) npWeekLength = 0; // if end date is in the middle of the week if (week.StartDate <= endDate.Value && week.EndDate >= endDate.Value) npWeekLength = (endDate.Value - week.StartDate).Days + 1; } var hoursOff = uomValue * (npWeekLength / (decimal)fcWeekLength); var percentOff = uomValue == 0 ? 0 : hoursOff / uomValue * 100; var distribution = new NonProjectTimeDistributionModel() { StartDate = Utils.ConvertToUnixDate(week.StartDate), EndDate = Utils.ConvertToUnixDate(week.EndDate), HoursOff = (int)hoursOff, PercentOff = Convert.ToInt32(percentOff), UOMValue = uomValue }; return distribution; } private NonProjectTimeSummaryItem[] ConvertNptQuery2Array(IEnumerable query) { return query.Select(x => new { x.NonProjectTimeId, x.NonProjectTimeName, x.PeopleResourceId, x.ExpenditureCategoryId, x.NonProjectTimeCategoryName, x.NonProjectTimeCategoryId, x.WeekEndingDate, x.HoursOff, x.IsTeamNonProjectTime, x.Rate }) .GroupBy(x => new { x.NonProjectTimeId, x.NonProjectTimeName, x.ExpenditureCategoryId, x.PeopleResourceId, x.NonProjectTimeCategoryId, x.NonProjectTimeCategoryName, x.WeekEndingDate, x.IsTeamNonProjectTime }) .Select(x => new NonProjectTimeSummaryItem { NonProjectTimeId = x.Key.NonProjectTimeId, NonProjectTimeName = x.Key.NonProjectTimeName, ExpenditureCategoryId = x.Key.ExpenditureCategoryId, PeopleResourceId = x.Key.PeopleResourceId, NonProjectTimeCategoryId = x.Key.NonProjectTimeCategoryId, NonProjectTimeCategoryName = x.Key.NonProjectTimeCategoryName, WeekEndingDate = x.Key.WeekEndingDate, HoursOff = x.Sum(r => r.HoursOff), Cost = x.Sum(r => (decimal)r.HoursOff * r.Rate), IsFromTeam = x.Key.IsTeamNonProjectTime }) .ToArray(); } #endregion } }