public async Task <ImportedShifts> ImportWeeklyShifts(int locationId, DateTimeOffset start) { var location = Db.Location.FirstOrDefault(l => l.Id == locationId); location.ThrowBusinessExceptionIfNull($"Couldn't find {nameof(Location)} with id: {locationId}."); var timezone = location?.Timezone; timezone.GetTimezone().ThrowBusinessExceptionIfNull("Timezone was invalid."); //We need to adjust to their start of the week, because it can differ depending on the TZ! var targetStartDate = start.ConvertToTimezone(timezone); var targetEndDate = targetStartDate.TranslateDateForDaylightSavings(timezone, 7); var sheriffsAvailableAtLocation = await SheriffService.GetSheriffsForShiftAvailabilityForLocation(locationId, targetStartDate, targetEndDate); var sheriffIds = sheriffsAvailableAtLocation.SelectDistinctToList(s => s.Id); var shiftsToImport = Db.Shift .Include(s => s.Location) .Include(s => s.Sheriff) .AsNoTracking() .In(sheriffIds, s => s.SheriffId) .Where(s => s.LocationId == locationId && s.ExpiryDate == null && s.StartDate < targetEndDate && targetStartDate < s.EndDate ); var importedShifts = await shiftsToImport.Select(shift => Db.DetachedClone(shift)).ToListAsync(); foreach (var shift in importedShifts) { shift.StartDate = shift.StartDate.TranslateDateForDaylightSavings(timezone, 7); shift.EndDate = shift.EndDate.TranslateDateForDaylightSavings(timezone, 7); } var overlaps = await GetShiftConflicts(importedShifts); var filteredImportedShifts = importedShifts.WhereToList(s => overlaps.All(o => o.Shift.Id != s.Id) && !overlaps.Any(ts => s.Id != ts.Shift.Id && ts.Shift.StartDate < s.EndDate && s.StartDate < ts.Shift.EndDate && ts.Shift.SheriffId == s.SheriffId)); filteredImportedShifts.ForEach(s => s.Id = 0); await Db.Shift.AddRangeAsync(filteredImportedShifts); await Db.SaveChangesAsync(); return(new ImportedShifts { ConflictMessages = overlaps.SelectMany(o => o.ConflictMessages).ToList(), Shifts = filteredImportedShifts }); }
private async Task <List <ShiftConflict> > CheckSheriffEventsOverlap(List <Shift> shifts) { var sheriffEventConflicts = new List <ShiftConflict>(); foreach (var shift in shifts) { var locationId = shift.LocationId; var sheriffs = await SheriffService.GetSheriffsForShiftAvailabilityForLocation(locationId, shift.StartDate, shift.EndDate, shift.SheriffId); var sheriff = sheriffs.FirstOrDefault(); var validationErrors = new List <string>(); if (sheriff == null) { var unavailableSheriff = await Db.Sheriff.AsNoTracking().FirstOrDefaultAsync(s => s.Id == shift.SheriffId); validationErrors.Add($"{unavailableSheriff?.LastName}, {unavailableSheriff?.FirstName} is not active in this location for {shift.StartDate.ConvertToTimezone(shift.Timezone).PrintFormatDate()} {shift.StartDate.ConvertToTimezone(shift.Timezone).PrintFormatTime(shift.Timezone)} to {shift.EndDate.ConvertToTimezone(shift.Timezone).PrintFormatTime(shift.Timezone)}"); } else { validationErrors.AddRange(sheriff !.AwayLocation.Where(aw => aw.LocationId != shift.LocationId) .Select(aw => PrintSheriffEventConflict <SheriffAwayLocation>(aw.Sheriff, aw.StartDate, aw.EndDate, aw.Timezone))); validationErrors.AddRange(sheriff.Leave.Select(aw => PrintSheriffEventConflict <SheriffLeave>( aw.Sheriff, aw.StartDate, aw.EndDate, aw.Timezone))); validationErrors.AddRange(sheriff.Training.Select(aw => PrintSheriffEventConflict <SheriffTraining>( aw.Sheriff, aw.StartDate, aw.EndDate, aw.Timezone))); } if (validationErrors.Any()) { sheriffEventConflicts.Add(new ShiftConflict { Shift = shift, ConflictMessages = validationErrors }); } } return(sheriffEventConflicts); }
/// <summary> /// This is used for Distribute Schedule, as well as the Shift Schedule page. /// </summary> public async Task <List <ShiftAvailability> > GetShiftAvailability(DateTimeOffset start, DateTimeOffset end, int locationId) { var sheriffs = await SheriffService.GetSheriffsForShiftAvailabilityForLocation(locationId, start, end); //Include sheriffs that have a shift, but their home location / away location doesn't match. //Grey out on the GUI if HomeLocationId and AwayLocation doesn't match. var sheriffIdsFromShifts = await Db.Shift.AsNoTracking() .Where(s => s.StartDate < end && start < s.EndDate && s.ExpiryDate == null && s.LocationId == locationId) .Select(s => s.SheriffId) .ToListAsync(); var sheriffsOutOfLocationWithShiftIds = sheriffIdsFromShifts.Except(sheriffs.Select(s => s.Id)); //Note their AwayLocation, Leave, Training should be entirely empty, this is intentional. var sheriffsOutOfLocationWithShift = await Db.Sheriff.AsNoTracking() .Include(s => s.HomeLocation) .In(sheriffsOutOfLocationWithShiftIds, s => s.Id) .Where(s => s.IsEnabled) .ToListAsync(); sheriffs = sheriffs.Concat(sheriffsOutOfLocationWithShift).ToList(); var shiftsForSheriffs = await GetShiftsForSheriffs(sheriffs.Select(s => s.Id), start, end); var sheriffEventConflicts = new List <ShiftAvailabilityConflict>(); sheriffs.ForEach(sheriff => { sheriffEventConflicts.AddRange(sheriff.AwayLocation.Select(s => new ShiftAvailabilityConflict { Conflict = ShiftConflictType.AwayLocation, SheriffId = sheriff.Id, Start = s.StartDate, End = s.EndDate, LocationId = s.LocationId, Location = s.Location, Timezone = s.Timezone, Comment = s.Comment })); sheriffEventConflicts.AddRange(sheriff.Leave.Select(s => new ShiftAvailabilityConflict { Conflict = ShiftConflictType.Leave, SheriffId = sheriff.Id, Start = s.StartDate, End = s.EndDate, Timezone = s.Timezone, SheriffEventType = s.LeaveType?.Code, Comment = s.Comment })); sheriffEventConflicts.AddRange(sheriff.Training.Select(s => new ShiftAvailabilityConflict { Conflict = ShiftConflictType.Training, SheriffId = sheriff.Id, Start = s.StartDate, End = s.EndDate, Timezone = s.Timezone, SheriffEventType = s.TrainingType?.Code, Comment = s.Note })); }); var existingShiftConflicts = shiftsForSheriffs.Select(s => new ShiftAvailabilityConflict { Conflict = ShiftConflictType.Scheduled, SheriffId = s.SheriffId, Location = s.Location, LocationId = s.LocationId, Start = s.StartDate, End = s.EndDate, ShiftId = s.Id, Timezone = s.Timezone, OvertimeHours = s.OvertimeHours, Comment = s.Comment }); //We've already included this information in the conflicts. sheriffs.ForEach(s => s.AwayLocation = null); sheriffs.ForEach(s => s.Leave = null); sheriffs.ForEach(s => s.Training = null); var allShiftConflicts = sheriffEventConflicts.Concat(existingShiftConflicts).ToList(); var lookupCode = await Db.LookupCode.AsNoTracking() .Where(lc => lc.Type == LookupTypes.SheriffRank) .Include(s => s.SortOrder) .ToListAsync(); return(sheriffs.SelectToList(s => new ShiftAvailability { Start = start, End = end, Sheriff = s, SheriffId = s.Id, Conflicts = allShiftConflicts.WhereToList(asc => asc.SheriffId == s.Id) }) .OrderBy(s => lookupCode.FirstOrDefault(so => so.Code == s.Sheriff.Rank) ?.SortOrder.FirstOrDefault() ?.SortOrder) .ThenBy(s => s.Sheriff.LastName) .ThenBy(s => s.Sheriff.FirstName) .ToList()); }