/// <summary> /// Executes the specified workflow. /// </summary> /// <param name="rockContext">The rock context.</param> /// <param name="action">The workflow action.</param> /// <param name="entity">The entity.</param> /// <param name="errorMessages">The error messages.</param> /// <returns></returns> /// <exception cref="System.NotImplementedException"></exception> public override bool Execute(RockContext rockContext, Rock.Model.WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState == null) { return(false); } int selectFromDaysBack = checkInState.CheckInType.AutoSelectDaysBack; var roomBalanceGroupTypes = GetAttributeValue(action, "RoomBalanceGrouptypes").SplitDelimitedValues().AsGuidList(); int roomBalanceOverride = GetAttributeValue(action, "BalancingOverride").AsIntegerOrNull() ?? 5; int maxAssignments = GetAttributeValue(action, "MaxAssignments").AsIntegerOrNull() ?? 5; var excludedLocations = GetAttributeValue(action, "ExcludedLocations").SplitDelimitedValues(whitespace: false) .Select(s => s.Trim()).ToList(); var personSpecialNeedsKey = string.Empty; var personSpecialNeedsGuid = GetAttributeValue(action, "PersonSpecialNeedsAttribute").AsGuid(); if (personSpecialNeedsGuid != Guid.Empty) { personSpecialNeedsKey = AttributeCache.Read(personSpecialNeedsGuid, rockContext).Key; } // log a warning if the attribute is missing or invalid if (string.IsNullOrWhiteSpace(personSpecialNeedsKey)) { action.AddLogEntry(string.Format("The Person Special Needs attribute is not selected or invalid for '{0}'.", action.ActionType.Name)); } var family = checkInState.CheckIn.Families.FirstOrDefault(f => f.Selected); if (family != null) { var cutoffDate = RockDateTime.Today.AddDays(selectFromDaysBack * -1); var attendanceService = new AttendanceService(rockContext); // only process people who have been here before foreach (var previousAttender in family.People.Where(p => p.Selected && !p.FirstTime)) { // get a list of this person's available grouptypes var availableGroupTypeIds = previousAttender.GroupTypes.Select(gt => gt.GroupType.Id).ToList(); // order by most recent attendance var lastDateAttendances = attendanceService.Queryable().Where(a => a.PersonAlias.PersonId == previousAttender.Person.Id && availableGroupTypeIds.Contains(a.Group.GroupTypeId) && a.StartDateTime >= cutoffDate && a.DidAttend == true) .OrderByDescending(a => a.StartDateTime).Take(maxAssignments) .ToList(); if (lastDateAttendances.Any()) { var assignmentsGiven = 0; // get the most recent day, then create assignments starting with the earliest attendance record var lastAttended = lastDateAttendances.Max(a => a.StartDateTime).Date; var numAttendances = lastDateAttendances.Count(a => a.StartDateTime >= lastAttended); foreach (var groupAttendance in lastDateAttendances.Where(a => a.StartDateTime >= lastAttended).OrderBy(a => a.Schedule.StartTimeOfDay)) { bool currentlyCheckedIn = false; var serviceCutoff = groupAttendance.StartDateTime; if (serviceCutoff > RockDateTime.Now.Date && groupAttendance.Schedule != null) { // calculate the service window to determine if people are still checked in var serviceTime = groupAttendance.StartDateTime.Date + groupAttendance.Schedule.StartTimeOfDay; var serviceStart = serviceTime.AddMinutes((groupAttendance.Schedule.CheckInStartOffsetMinutes ?? 0) * -1.0); serviceCutoff = serviceTime.AddMinutes((groupAttendance.Schedule.CheckInEndOffsetMinutes ?? 0)); currentlyCheckedIn = RockDateTime.Now > serviceStart && RockDateTime.Now < serviceCutoff; } // override exists in case they are currently checked in or have special needs bool useCheckinOverride = currentlyCheckedIn || previousAttender.Person.GetAttributeValue(personSpecialNeedsKey).AsBoolean(); // get a list of room balanced grouptype ID's since CheckInGroup model is a shallow clone var roomBalanceGroupTypeIds = previousAttender.GroupTypes.Where(gt => roomBalanceGroupTypes.Contains(gt.GroupType.Guid)) .Select(gt => gt.GroupType.Id).ToList(); // start with filtered groups unless they have abnormal age and grade parameters (1%) var groupType = previousAttender.GroupTypes.FirstOrDefault(gt => gt.GroupType.Id == groupAttendance.Group.GroupTypeId && (!gt.ExcludedByFilter || useCheckinOverride)); if (groupType != null) { // assigning the right schedule depends on prior attendance & currently available schedules being sorted var orderedSchedules = groupType.Groups.SelectMany(g => g.Locations.SelectMany(l => l.Schedules)) .DistinctBy(s => s.Schedule.Id).OrderBy(s => s.Schedule.StartTimeOfDay) .Select(s => s.Schedule.Id).ToList(); int?currentScheduleId = null; if (orderedSchedules.Count == 1) { currentScheduleId = orderedSchedules.FirstOrDefault(); } else if (currentlyCheckedIn) { // always pick the schedule they're currently checked into currentScheduleId = orderedSchedules.Where(s => s == groupAttendance.ScheduleId).FirstOrDefault(); } else { // sort the earliest schedule for the current grouptype, then skip the number of assignments already given (multiple services) currentScheduleId = groupType.AvailableForSchedule .OrderBy(d => orderedSchedules.IndexOf(d)) .Skip(assignmentsGiven).FirstOrDefault(); } CheckInGroup group = null; if (groupType.Groups.Count == 1) { // only a single group is open group = groupType.Groups.FirstOrDefault(g => !g.ExcludedByFilter || useCheckinOverride); } else { // pick the group they last attended, as long as it's open or what they're currently checked into group = groupType.Groups.FirstOrDefault(g => g.Group.Id == groupAttendance.GroupId && (!g.ExcludedByFilter || useCheckinOverride)); // room balance only on new check-ins and only for the current service if (group != null && currentScheduleId != null && roomBalanceGroupTypeIds.Contains(group.Group.GroupTypeId) && !excludedLocations.Contains(group.Group.Name) && !useCheckinOverride) { // make sure balanced rooms are open for the current service var currentAttendance = group.Locations.Where(l => l.AvailableForSchedule.Contains((int)currentScheduleId)) .Select(l => Helpers.ReadAttendanceBySchedule(l.Location.Id, currentScheduleId)).Sum(); var lowestAttendedGroup = groupType.Groups.Where(g => g.AvailableForSchedule.Contains((int)currentScheduleId)) .Where(g => !g.ExcludedByFilter && !excludedLocations.Contains(g.Group.Name)) .Select(g => new { Group = g, Attendance = g.Locations.Select(l => Helpers.ReadAttendanceBySchedule(l.Location.Id, currentScheduleId)).Sum() }) .OrderBy(g => g.Attendance) .FirstOrDefault(); if (lowestAttendedGroup != null && lowestAttendedGroup.Attendance < (currentAttendance - roomBalanceOverride + 1)) { group = lowestAttendedGroup.Group; } } } if (group != null) { CheckInLocation location = null; if (group.Locations.Count == 1) { // only a single location is open location = group.Locations.FirstOrDefault(l => !l.ExcludedByFilter || useCheckinOverride); } else { // pick the location they last attended, as long as it's open or what they're currently checked into location = group.Locations.FirstOrDefault(l => l.Location.Id == groupAttendance.LocationId && (!l.ExcludedByFilter || useCheckinOverride)); // room balance only on new check-ins and only for the current service if (location != null && currentScheduleId != null && roomBalanceGroupTypeIds.Contains(group.Group.GroupTypeId) && !excludedLocations.Contains(location.Location.Name) && !useCheckinOverride) { var currentAttendance = Helpers.ReadAttendanceBySchedule(location.Location.Id, currentScheduleId); var lowestAttendedLocation = group.Locations.Where(l => l.AvailableForSchedule.Contains((int)currentScheduleId)) .Where(l => !l.ExcludedByFilter && !excludedLocations.Contains(l.Location.Name)) .Select(l => new { Location = l, Attendance = Helpers.ReadAttendanceBySchedule(l.Location.Id, currentScheduleId) }) .OrderBy(l => l.Attendance) .FirstOrDefault(); if (lowestAttendedLocation != null && lowestAttendedLocation.Attendance < (currentAttendance - roomBalanceOverride + 1)) { location = lowestAttendedLocation.Location; } } } if (location != null) { // the current schedule could exist on multiple locations, so pick the one owned by this location // if the current schedule just closed, get the first available schedule at this location CheckInSchedule schedule = location.Schedules.OrderByDescending(s => s.Schedule.Id == currentScheduleId).FirstOrDefault(); if (schedule != null) { // it's impossible to currently be checked in unless these match exactly if (group.Group.Id == groupAttendance.GroupId && location.Location.Id == groupAttendance.LocationId && schedule.Schedule.Id == groupAttendance.ScheduleId) { // checkout feature either removes the attendance or sets the EndDateTime var endOfCheckinWindow = groupAttendance.EndDateTime ?? serviceCutoff; schedule.LastCheckIn = endOfCheckinWindow; location.LastCheckIn = endOfCheckinWindow; group.LastCheckIn = endOfCheckinWindow; groupType.LastCheckIn = endOfCheckinWindow; previousAttender.LastCheckIn = endOfCheckinWindow; } // finished finding assignment, verify everything is selected schedule.Selected = true; schedule.PreSelected = true; location.Selected = true; location.PreSelected = true; group.Selected = true; group.PreSelected = true; groupType.Selected = true; groupType.PreSelected = true; previousAttender.Selected = true; previousAttender.PreSelected = true; assignmentsGiven++; } } } } } } } } return(true); }
/// <summary> /// Executes the specified workflow. /// </summary> /// <param name="rockContext">The rock context.</param> /// <param name="action">The workflow action.</param> /// <param name="entity">The entity.</param> /// <param name="errorMessages">The error messages.</param> /// <returns></returns> /// <exception cref="System.NotImplementedException"></exception> public override bool Execute(RockContext rockContext, Rock.Model.WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState == null) { return(false); } var roomBalanceGroupTypes = GetAttributeValue(action, "RoomBalanceGrouptypes").SplitDelimitedValues().AsGuidList(); bool useGroupMembership = GetAttributeValue(action, "PrioritizeGroupMembership").AsBoolean(); int roomBalanceOverride = GetAttributeValue(action, "BalancingOverride").AsIntegerOrNull() ?? 5; var excludedLocations = GetAttributeValue(action, "ExcludedLocations").SplitDelimitedValues(false) .Select(s => s.Trim()); // get admin-selected attribute keys instead of using a hardcoded key var personSpecialNeedsKey = string.Empty; var personSpecialNeedsGuid = GetAttributeValue(action, "PersonSpecialNeedsAttribute").AsGuid(); if (personSpecialNeedsGuid != Guid.Empty) { personSpecialNeedsKey = AttributeCache.Read(personSpecialNeedsGuid, rockContext).Key; } var groupSpecialNeedsKey = string.Empty; var groupSpecialNeedsGuid = GetAttributeValue(action, "GroupSpecialNeedsAttribute").AsGuid(); if (personSpecialNeedsGuid != Guid.Empty) { groupSpecialNeedsKey = AttributeCache.Read(groupSpecialNeedsGuid, rockContext).Key; } var groupAgeRangeKey = string.Empty; var groupAgeRangeGuid = GetAttributeValue(action, "GroupAgeRangeAttribute").AsGuid(); if (personSpecialNeedsGuid != Guid.Empty) { groupAgeRangeKey = AttributeCache.Read(groupAgeRangeGuid, rockContext).Key; } var groupGradeRangeKey = string.Empty; var groupGradeRangeGuid = GetAttributeValue(action, "GroupGradeRangeAttribute").AsGuid(); if (personSpecialNeedsGuid != Guid.Empty) { groupGradeRangeKey = AttributeCache.Read(groupGradeRangeGuid, rockContext).Key; } // log a warning if any of the attributes are missing or invalid if (string.IsNullOrWhiteSpace(personSpecialNeedsKey)) { action.AddLogEntry(string.Format("The Person Special Needs attribute is not selected or invalid for '{0}'.", action.ActionType.Name)); } if (string.IsNullOrWhiteSpace(groupSpecialNeedsKey)) { action.AddLogEntry(string.Format("The Group Special Needs attribute is not selected or invalid for '{0}'.", action.ActionType.Name)); } if (string.IsNullOrWhiteSpace(groupAgeRangeKey)) { action.AddLogEntry(string.Format("The Group Age Range attribute is not selected or invalid for '{0}'.", action.ActionType.Name)); } if (string.IsNullOrWhiteSpace(groupGradeRangeKey)) { action.AddLogEntry(string.Format("The Group Grade Range attribute is not selected or invalid for '{0}'.", action.ActionType.Name)); } var family = checkInState.CheckIn.Families.FirstOrDefault(f => f.Selected); if (family != null) { // don't process people who already have assignments foreach (var person in family.People.Where(f => f.Selected && !f.GroupTypes.Any(gt => gt.Selected))) { decimal baseVariance = 100; char[] delimiter = { ',' }; // check if this person has special needs var hasSpecialNeeds = person.Person.GetAttributeValue(personSpecialNeedsKey).AsBoolean(); // get a list of room balanced grouptype ID's since CheckInGroup model is a shallow clone var roomBalanceGroupTypeIds = person.GroupTypes.Where(gt => roomBalanceGroupTypes.Contains(gt.GroupType.Guid)) .Select(gt => gt.GroupType.Id).ToList(); if (person.GroupTypes.Count > 0) { CheckInGroupType bestGroupType = null; IEnumerable <CheckInGroup> validGroups; if (person.GroupTypes.Count == 1) { bestGroupType = person.GroupTypes.FirstOrDefault(); validGroups = bestGroupType.Groups; } else { // Start with unfiltered groups since one criteria may not match exactly ( SN > Grade > Age ) validGroups = person.GroupTypes.SelectMany(gt => gt.Groups); } // check how many groups exist without getting the whole list int numValidGroups = validGroups.Take(2).Count(); if (numValidGroups > 0) { CheckInGroup bestGroup = null; IEnumerable <CheckInLocation> validLocations; if (numValidGroups == 1) { bestGroup = validGroups.FirstOrDefault(); validLocations = bestGroup.Locations; } else { // Select by group assignment first if (useGroupMembership) { var personAssignments = new GroupMemberService(rockContext).GetByPersonId(person.Person.Id) .Select(gm => gm.Group.Id).ToList(); if (personAssignments.Count > 0) { bestGroup = validGroups.FirstOrDefault(g => personAssignments.Contains(g.Group.Id)); } } // Select group by best fit if (bestGroup == null) { // Check age and special needs CheckInGroup closestAgeGroup = null; CheckInGroup closestNeedsGroup = null; var ageGroups = validGroups.Where(g => g.Group.AttributeValues.ContainsKey(groupAgeRangeKey) && g.Group.AttributeValues[groupAgeRangeKey].Value != null && g.Group.AttributeValues.ContainsKey(personSpecialNeedsKey) == hasSpecialNeeds ) .ToList() .Select(g => new { Group = g, AgeRange = g.Group.AttributeValues[groupAgeRangeKey].Value .Split(delimiter, StringSplitOptions.None) .Where(av => !string.IsNullOrEmpty(av)) .Select(av => av.AsType <decimal>()) }) .ToList(); if (ageGroups.Count > 0) { if (person.Person.Age != null) { baseVariance = 100; decimal personAge = (decimal)person.Person.AgePrecise; foreach (var ageGroup in ageGroups.Where(g => g.AgeRange.Any())) { var minAge = ageGroup.AgeRange.First(); var maxAge = ageGroup.AgeRange.Last(); var ageVariance = maxAge - minAge; if (maxAge >= personAge && minAge <= personAge && ageVariance < baseVariance) { closestAgeGroup = ageGroup.Group; baseVariance = ageVariance; if (hasSpecialNeeds) { closestNeedsGroup = closestAgeGroup; } } } } else if (hasSpecialNeeds) { // person has special needs but not an age, assign to first special needs group closestNeedsGroup = ageGroups.FirstOrDefault().Group; } } // Check grade CheckInGroup closestGradeGroup = null; if (person.Person.GradeOffset != null) { var gradeValues = DefinedTypeCache.Read(new Guid(Rock.SystemGuid.DefinedType.SCHOOL_GRADES)).DefinedValues; var gradeGroups = validGroups.Where(g => g.Group.AttributeValues.ContainsKey(groupGradeRangeKey) && g.Group.AttributeValues[groupGradeRangeKey].Value != null) .ToList() .Select(g => new { Group = g, GradeOffsets = g.Group.AttributeValues[groupGradeRangeKey].Value .Split(delimiter, StringSplitOptions.None) .Where(av => !string.IsNullOrEmpty(av)) .Select(av => gradeValues.FirstOrDefault(v => v.Guid == new Guid(av))) .Select(av => av.Value.AsDecimal()) }) .ToList(); // Only check groups that have valid grade offsets if (person.Person.GradeOffset != null && gradeGroups.Count > 0) { baseVariance = 100; decimal gradeOffset = (decimal)person.Person.GradeOffset.Value; foreach (var gradeGroup in gradeGroups.Where(g => g.GradeOffsets.Any())) { var minGradeOffset = gradeGroup.GradeOffsets.First(); var maxGradeOffset = gradeGroup.GradeOffsets.Last(); var gradeVariance = minGradeOffset - maxGradeOffset; if (minGradeOffset >= gradeOffset && maxGradeOffset <= gradeOffset && gradeVariance < baseVariance) { closestGradeGroup = gradeGroup.Group; baseVariance = gradeVariance; } } /* ======================================================== * * optional scenario: find the next closest grade group * ========================================================= * * if (grade > max) * grade - max * else if (grade < min) * min - grade * else 0; * * // add a tiny variance to offset larger groups: * result += ((max - min)/100) * ========================================================= */ } } // Assignment priority: Group Membership, then Ability, then Grade, then Age, then the first non-excluded group // NOTE: if group member is prioritized (and membership exists) this section is skipped entirely bestGroup = closestNeedsGroup ?? closestGradeGroup ?? closestAgeGroup ?? validGroups.FirstOrDefault(g => !g.ExcludedByFilter); // room balance if they fit into multiple groups if (bestGroup != null && roomBalanceGroupTypeIds.Contains(bestGroup.Group.GroupTypeId)) { int?bestScheduleId = null; var availableSchedules = validGroups.SelectMany(g => g.Locations.SelectMany(l => l.Schedules)).DistinctBy(s => s.Schedule.Id).ToList(); if (availableSchedules.Any()) { bestScheduleId = availableSchedules.OrderBy(s => s.StartTime).Select(s => s.Schedule.Id).FirstOrDefault(); } if (bestScheduleId != null) { validGroups = validGroups.Where(g => g.AvailableForSchedule.Contains((int)bestScheduleId)); } var currentGroupAttendance = bestGroup.Locations.Select(l => Helpers.ReadAttendanceBySchedule(l.Location.Id, bestScheduleId)).Sum(); var lowestGroup = validGroups.Where(g => !g.ExcludedByFilter && !excludedLocations.Contains(g.Group.Name)) .Select(g => new { Group = g, Attendance = g.Locations.Select(l => Helpers.ReadAttendanceBySchedule(l.Location.Id, bestScheduleId)).Sum() }) .OrderBy(g => g.Attendance) .FirstOrDefault(); if (lowestGroup != null && lowestGroup.Attendance < (currentGroupAttendance - roomBalanceOverride)) { bestGroup = lowestGroup.Group; } } } validLocations = bestGroup.Locations; } // check how many locations exist without getting the whole list int numValidLocations = validLocations.Take(2).Count(); if (numValidLocations > 0) { CheckInLocation bestLocation = null; if (numValidLocations == 1) { bestLocation = validLocations.FirstOrDefault(); } else { var filteredLocations = validLocations.Where(l => !l.ExcludedByFilter && !excludedLocations.Contains(l.Location.Name)); // room balance if they fit into multiple locations if (roomBalanceGroupTypeIds.Contains(bestGroup.Group.GroupTypeId)) { int?bestScheduleId = null; var availableSchedules = filteredLocations.SelectMany(l => l.Schedules).DistinctBy(s => s.Schedule.Id).ToList(); if (availableSchedules.Any()) { bestScheduleId = availableSchedules.OrderBy(s => s.StartTime).Select(s => s.Schedule.Id).FirstOrDefault(); } if (bestScheduleId != null) { filteredLocations = filteredLocations.Where(l => l.AvailableForSchedule.Contains((int)bestScheduleId)); } filteredLocations = filteredLocations.OrderBy(l => Helpers.ReadAttendanceBySchedule(l.Location.Id, bestScheduleId)); } bestLocation = filteredLocations.FirstOrDefault(); } if (bestLocation != null && bestLocation.Schedules.Any()) { // finished finding assignment, verify we can select everything var bestSchedule = bestLocation.Schedules.OrderBy(s => s.Schedule.StartTimeOfDay).FirstOrDefault(); if (bestSchedule != null) { bestSchedule.Selected = true; bestSchedule.PreSelected = true; if (bestLocation != null) { bestLocation.PreSelected = true; bestLocation.Selected = true; if (bestGroup != null) { bestGroup.PreSelected = true; bestGroup.Selected = true; bestGroupType = person.GroupTypes.FirstOrDefault(gt => gt.GroupType.Id == bestGroup.Group.GroupTypeId); if (bestGroupType != null) { bestGroupType.Selected = true; bestGroupType.PreSelected = true; person.Selected = true; person.PreSelected = true; } } } } } } } } } } return(true); }
/// <summary> /// Executes the specified workflow. /// </summary> /// <param name="rockContext">The rock context.</param> /// <param name="action">The workflow action.</param> /// <param name="entity">The entity.</param> /// <param name="errorMessages">The error messages.</param> /// <returns></returns> /// <exception cref="System.NotImplementedException"></exception> public override bool Execute(RockContext rockContext, Rock.Model.WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState == null) { return(false); } int selectFromDaysBack = checkInState.CheckInType.AutoSelectDaysBack; var roomBalanceGroupTypes = GetAttributeValue(action, "RoomBalanceGrouptypes").SplitDelimitedValues().AsGuidList(); int roomBalanceOverride = GetAttributeValue(action, "BalancingOverride").AsIntegerOrNull() ?? 5; int maxAssignments = GetAttributeValue(action, "MaxAssignments").AsIntegerOrNull() ?? 5; var excludedLocations = GetAttributeValue(action, "ExcludedLocations").SplitDelimitedValues(whitespace: false) .Select(s => s.Trim()); // get the admin-selected attribute key instead of using a hardcoded key var personSpecialNeedsKey = string.Empty; var personSpecialNeedsGuid = GetAttributeValue(action, "PersonSpecialNeedsAttribute").AsGuid(); if (personSpecialNeedsGuid != Guid.Empty) { personSpecialNeedsKey = AttributeCache.Read(personSpecialNeedsGuid, rockContext).Key; } // log a warning if the attribute is missing or invalid if (string.IsNullOrWhiteSpace(personSpecialNeedsKey)) { action.AddLogEntry(string.Format("The Person Special Needs attribute is not selected or invalid for '{0}'.", action.ActionType.Name)); } var family = checkInState.CheckIn.Families.FirstOrDefault(f => f.Selected); if (family != null) { var cutoffDate = RockDateTime.Today.AddDays(selectFromDaysBack * -1); var attendanceService = new AttendanceService(rockContext); // only process people who have been here before foreach (var previousAttender in family.People.Where(p => p.Selected && !p.FirstTime)) { // get a list of this person's available grouptypes var availableGroupTypeIds = previousAttender.GroupTypes.Select(gt => gt.GroupType.Id).ToList(); var lastDateAttendances = attendanceService.Queryable().Where(a => a.PersonAlias.PersonId == previousAttender.Person.Id && availableGroupTypeIds.Contains(a.Group.GroupTypeId) && a.StartDateTime >= cutoffDate && a.DidAttend == true) .OrderByDescending(a => a.StartDateTime).Take(maxAssignments) .ToList(); if (lastDateAttendances.Any()) { var lastAttended = lastDateAttendances.Max(a => a.StartDateTime).Date; var numAttendances = lastDateAttendances.Count(a => a.StartDateTime >= lastAttended); foreach (var groupAttendance in lastDateAttendances.Where(a => a.StartDateTime >= lastAttended)) { bool currentlyCheckedIn = false; var serviceCutoff = groupAttendance.StartDateTime; if (serviceCutoff > RockDateTime.Now.Date) { // calculate the service window to determine if people are still checked in var serviceTime = groupAttendance.StartDateTime.Date + groupAttendance.Schedule.NextStartDateTime.Value.TimeOfDay; var serviceStart = serviceTime.AddMinutes((groupAttendance.Schedule.CheckInStartOffsetMinutes ?? 0) * -1.0); serviceCutoff = serviceTime.AddMinutes((groupAttendance.Schedule.CheckInEndOffsetMinutes ?? 0)); currentlyCheckedIn = RockDateTime.Now > serviceStart && RockDateTime.Now < serviceCutoff; } // override exists in case they are currently checked in or have special needs bool useCheckinOverride = currentlyCheckedIn || previousAttender.Person.GetAttributeValue(personSpecialNeedsKey).AsBoolean(); // get a list of room balanced grouptype ID's since CheckInGroup model is a shallow clone var roomBalanceGroupTypeIds = previousAttender.GroupTypes.Where(gt => roomBalanceGroupTypes.Contains(gt.GroupType.Guid)) .Select(gt => gt.GroupType.Id).ToList(); // Start with filtered groups unless they have abnormal age and grade parameters (1%) var groupType = previousAttender.GroupTypes.FirstOrDefault(gt => gt.GroupType.Id == groupAttendance.Group.GroupTypeId && (!gt.ExcludedByFilter || useCheckinOverride)); if (groupType != null) { CheckInGroup group = null; if (groupType.Groups.Count == 1) { // Only a single group is open group = groupType.Groups.FirstOrDefault(g => !g.ExcludedByFilter || useCheckinOverride); } else { // Pick the group they last attended, as long as it's open or what they're currently checked into group = groupType.Groups.FirstOrDefault(g => g.Group.Id == groupAttendance.GroupId && (!g.ExcludedByFilter || useCheckinOverride)); // room balance only on new check-ins if (group != null && roomBalanceGroupTypeIds.Contains(group.Group.GroupTypeId) && !useCheckinOverride) { //TODO: use KioskLocationAttendance and group.AvailableForSchedule to room balance by service time attendance, not the entire day var currentAttendance = group.Locations.Select(l => KioskLocationAttendance.Read(l.Location.Id).CurrentCount).Sum(); var lowestAttendedGroup = groupType.Groups.Where(g => !g.ExcludedByFilter && !excludedLocations.Contains(g.Group.Name)) .Select(g => new { Group = g, Attendance = g.Locations.Select(l => KioskLocationAttendance.Read(l.Location.Id).CurrentCount).Sum() }) .OrderBy(g => g.Attendance) .FirstOrDefault(); if (lowestAttendedGroup != null && lowestAttendedGroup.Attendance < (currentAttendance - roomBalanceOverride + 1)) { group = lowestAttendedGroup.Group; } } } if (group != null) { CheckInLocation location = null; if (group.Locations.Count == 1) { // Only a single location is open location = group.Locations.FirstOrDefault(l => !l.ExcludedByFilter || useCheckinOverride); } else { // Pick the location they last attended, as long as it's open or what they're currently checked into location = group.Locations.FirstOrDefault(l => l.Location.Id == groupAttendance.LocationId && (!l.ExcludedByFilter || useCheckinOverride)); // room balance only on new check-ins if (location != null && roomBalanceGroupTypeIds.Contains(group.Group.GroupTypeId) && !useCheckinOverride) { //TODO: use KioskLocationAttendance and location.AvailableForSchedule to room balance by service time attendance, not the entire day var currentAttendance = KioskLocationAttendance.Read(location.Location.Id).CurrentCount; var lowestAttendedLocation = group.Locations.Where(l => !l.ExcludedByFilter && !excludedLocations.Contains(l.Location.Name)) .Select(l => new { Location = l, Attendance = KioskLocationAttendance.Read(l.Location.Id).CurrentCount }) .OrderBy(l => l.Attendance) .FirstOrDefault(); if (lowestAttendedLocation != null && lowestAttendedLocation.Attendance < (currentAttendance - roomBalanceOverride + 1)) { location = lowestAttendedLocation.Location; } } } if (location != null) { CheckInSchedule schedule = null; if (location.Schedules.Count == 1) { schedule = location.Schedules.FirstOrDefault(s => !s.ExcludedByFilter || useCheckinOverride); } else { // if assigning to multiple services or currently checked in (not SN, otherwise they would get the wrong auto-schedule) if (numAttendances > 1 || currentlyCheckedIn) { // pick what they last attended last schedule = location.Schedules.FirstOrDefault(s => s.Schedule.Id == groupAttendance.ScheduleId && (!s.ExcludedByFilter || useCheckinOverride)); } // otherwise pick the earliest available schedule schedule = schedule ?? location.Schedules.OrderBy(s => s.Schedule.StartTimeOfDay).FirstOrDefault(s => !s.ExcludedByFilter); } if (schedule != null) { // it's impossible to currently be checked in unless these match exactly if (group.Group.Id == groupAttendance.GroupId && location.Location.Id == groupAttendance.LocationId && schedule.Schedule.Id == groupAttendance.ScheduleId) { // Checkout would've removed the attendance or set the EndDateTime var endOfCheckinWindow = groupAttendance.EndDateTime ?? serviceCutoff; schedule.LastCheckIn = endOfCheckinWindow; location.LastCheckIn = endOfCheckinWindow; group.LastCheckIn = endOfCheckinWindow; groupType.LastCheckIn = endOfCheckinWindow; previousAttender.LastCheckIn = endOfCheckinWindow; } // finished finding assignment, verify everything is selected schedule.Selected = true; schedule.PreSelected = true; location.Selected = true; location.PreSelected = true; group.Selected = true; group.PreSelected = true; groupType.Selected = true; groupType.PreSelected = true; groupType.Selected = true; groupType.PreSelected = true; previousAttender.Selected = true; previousAttender.PreSelected = true; } } } } } } } } return(true); }