/// <summary> /// Toggles on or off one check-in GroupLocationSchedule (it will deselct others) /// </summary> /// <param name="chGroup">Checkin Group</param> /// <param name="chLocation">Checkin Location</param> /// <param name="chSchedule">Checkin Schedule</param> /// <param name="alreadySelected">Is the Group Location Schedule already selected</param> private void ToggleClass(CheckInGroup chGroup, CheckInLocation chLocation, CheckInSchedule chSchedule, bool alreadySelected) { foreach (var groupType in CurrentCheckInState.CheckIn.CurrentPerson.GroupTypes) { groupType.Selected = true; foreach (var group in groupType.Groups) { group.Selected = false; foreach (var location in group.Locations) { location.Selected = false; foreach (var schedule in location.Schedules) { schedule.Selected = false; } } } } if (!alreadySelected) { chGroup.Selected = true; chLocation.Selected = true; chSchedule.Selected = true; btnCheckin.Visible = true; } SaveState(); ShowPersonCheckin(); }
/// <summary> /// Binds the schedules. /// </summary> /// <param name="person">The person.</param> protected void BindSchedules(List <CheckInGroupType> groupTypes) { int groupTypeId = ViewState["groupTypeId"].ToString().AsType <int>(); int locationId = ViewState["locationId"].ToString().AsType <int>(); CheckInGroupType groupType = null; if (groupTypes.Any(gt => gt.GroupType.Id == groupTypeId)) { groupType = groupTypes.Where(gt => gt.GroupType.Id == groupTypeId).FirstOrDefault(); } else { groupType = groupTypes.FirstOrDefault(); } CheckInLocation location = null; var locations = groupType.Groups.SelectMany(g => g.Locations).ToList(); if (locationId > 0) { location = locations.Where(l => l.Location.Id == locationId).FirstOrDefault(); } else { location = locations.FirstOrDefault(); } rSchedule.DataSource = location.Schedules.ToList(); rSchedule.DataBind(); pnlSchedules.Update(); }
/// <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, WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState != null) { var groupService = new GroupService(rockContext); foreach (var family in checkInState.CheckIn.Families.Where(f => f.Selected).ToList()) { foreach (var person in family.People) { var checkinGroupTypes = new List <CheckInGroupType>(); foreach (var id in checkInState.ConfiguredGroupTypes) { var cgt = new CheckInGroupType(); cgt.GroupType = GroupTypeCache.Get(id); checkinGroupTypes.Add(cgt); var groups = groupService.Queryable().Where(g => g.GroupTypeId == id); List <CheckInGroup> checkinGroups = new List <CheckInGroup>(); foreach (var group in groups) { var cg = new CheckInGroup(); cg.Group = group; group.LoadAttributes(); checkinGroups.Add(cg); var groupLocations = group.GroupLocations; List <CheckInLocation> checkinLocations = new List <CheckInLocation>(); foreach (var groupLocation in groupLocations) { var cl = new CheckInLocation(); cl.Location = groupLocation.Location; checkinLocations.Add(cl); var schedules = new List <CheckInSchedule>(); foreach (var schedule in groupLocation.Schedules.ToList()) { if (!schedule.WasCheckInActive(RockDateTime.Now)) { continue; } var cs = new CheckInSchedule(); cs.Schedule = schedule; schedules.Add(cs); } cl.Schedules = schedules; } cg.Locations = checkinLocations; } cgt.Groups = checkinGroups; } person.GroupTypes = checkinGroupTypes; } } return(true); } errorMessages.Add($"Attempted to run {this.GetType().GetFriendlyTypeName()} in check-in, but the check-in state was null."); return(false); }
/// <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, WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState != null) { var groupService = new GroupService(rockContext); foreach (var family in checkInState.CheckIn.Families.Where(f => f.Selected).ToList()) { foreach (var person in family.People) { var checkinGroupTypes = new List <CheckInGroupType>(); foreach (var id in checkInState.ConfiguredGroupTypes) { var cgt = new CheckInGroupType(); cgt.GroupType = GroupTypeCache.Get(id); checkinGroupTypes.Add(cgt); var groups = groupService.Queryable().Where(g => g.GroupTypeId == id); List <CheckInGroup> checkinGroups = new List <CheckInGroup>(); foreach (var group in groups.Where(g => g.IsActive)) { var cg = new CheckInGroup(); cg.Group = group; group.LoadAttributes(); checkinGroups.Add(cg); var groupLocations = group.GroupLocations; List <CheckInLocation> checkinLocations = new List <CheckInLocation>(); foreach (var groupLocation in groupLocations) { var cl = new CheckInLocation(); cl.Location = groupLocation.Location; cl.CampusId = cl.Location.CampusId; checkinLocations.Add(cl); var schedules = new List <CheckInSchedule>(); foreach (var schedule in groupLocation.Schedules) { var cs = new CheckInSchedule(); cs.Schedule = schedule; schedules.Add(cs); } cl.Schedules = schedules; } cg.Locations = checkinLocations; } cgt.Groups = checkinGroups; } person.GroupTypes = checkinGroupTypes; } } return(true); } return(false); }
/// <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, Model.WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState != null) { bool loadAll = GetAttributeValue(action, "LoadAll").AsBoolean(); foreach (var family in checkInState.CheckIn.GetFamilies(true)) { foreach (var person in family.GetPeople(!loadAll)) { foreach (var groupType in person.GetGroupTypes(!loadAll).ToList()) { var kioskGroupType = checkInState.Kiosk.ActiveGroupTypes(checkInState.ConfiguredGroupTypes) .Where(g => g.GroupType.Id == groupType.GroupType.Id) .FirstOrDefault(); if (kioskGroupType != null) { foreach (var group in groupType.GetGroups(!loadAll)) { foreach (var kioskGroup in kioskGroupType.KioskGroups .Where(g => g.Group.Id == group.Group.Id && g.IsCheckInActive) .ToList()) { foreach (var kioskLocation in kioskGroup.KioskLocations.Where(l => l.IsCheckInActive && l.IsActiveAndNotFull)) { if (!group.Locations.Any(l => l.Location.Id == kioskLocation.Location.Id)) { var checkInLocation = new CheckInLocation(); checkInLocation.Location = kioskLocation.Location.Clone(false); checkInLocation.Location.CopyAttributesFrom(kioskLocation.Location); checkInLocation.CampusId = kioskLocation.CampusId; checkInLocation.Order = kioskLocation.Order; group.Locations.Add(checkInLocation); } } } } } } } } return(true); } return(false); }
/// <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, Model.WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState != null) { bool loadAll = false; if (bool.TryParse(GetAttributeValue(action, "LoadAll"), out loadAll) && loadAll) { loadAll = true; } foreach (var family in checkInState.CheckIn.Families.Where(f => f.Selected).ToList()) { foreach (var person in family.People.Where(p => p.Selected || loadAll).ToList()) { foreach (var groupType in person.GroupTypes.Where(t => t.Selected || loadAll).ToList()) { var kioskGroupType = checkInState.Kiosk.FilteredGroupTypes(checkInState.ConfiguredGroupTypes).Where(g => g.GroupType.Id == groupType.GroupType.Id).FirstOrDefault(); if (kioskGroupType != null) { foreach (var group in groupType.Groups.Where(g => g.Selected || loadAll).ToList()) { foreach (var kioskGroup in kioskGroupType.KioskGroups.Where(g => g.Group.Id == group.Group.Id).ToList()) { foreach (var kioskLocation in kioskGroup.KioskLocations) { if (!group.Locations.Any(l => l.Location.Id == kioskLocation.Location.Id)) { var checkInLocation = new CheckInLocation(); checkInLocation.Location = kioskLocation.Location.Clone(false); checkInLocation.Location.CopyAttributesFrom(kioskLocation.Location); checkInLocation.CampuId = kioskLocation.CampusId; group.Locations.Add(checkInLocation); } } } } } } } } return(true); } return(false); }
/// <summary> /// Gets the attendance count for all of the schedules for a location. This will show on the schedule buttons. /// </summary> /// <param name="location"></param> protected void GetScheduleAttendance(CheckInLocation location) { if (location != null) { var rockContext = new RockContext(); var attendanceService = new AttendanceService(rockContext); var attendanceQuery = attendanceService.GetByDateAndLocation(DateTime.Now, location.Location.Id); ScheduleAttendanceList.Clear(); foreach (var schedule in location.Schedules) { var attendance = new ScheduleAttendance(); attendance.ScheduleId = schedule.Schedule.Id; attendance.AttendanceCount = attendanceQuery.Where(l => l.ScheduleId == attendance.ScheduleId).Count(); ScheduleAttendanceList.Add(attendance); } } }
/// <summary> /// Handles the Delete event of the gPersonList control. /// </summary> /// <param name="sender">The source of the event.</param> /// <param name="e">The <see cref="RowEventArgs" /> instance containing the event data.</param> protected void gPersonList_Delete(object sender, RowEventArgs e) { var dataKeyValues = gPersonList.DataKeys[e.RowIndex].Values; var personId = Convert.ToInt32(dataKeyValues["PersonId"]); var locationId = Convert.ToInt32(dataKeyValues["LocationId"]); var scheduleId = Convert.ToInt32(dataKeyValues["ScheduleId"]); var selectedPerson = CurrentCheckInState.CheckIn.Families.Where(f => f.Selected).FirstOrDefault() .People.Where(p => p.Person.Id == personId).FirstOrDefault(); var selectedGroups = selectedPerson.GroupTypes.Where(gt => gt.Selected) .SelectMany(gt => gt.Groups.Where(g => g.Selected)); CheckInGroup selectedGroup = selectedGroups.Where(g => g.Selected && g.Locations.Any(l => l.Location.Id == locationId && l.Schedules.Any(s => s.Schedule.Id == scheduleId))).FirstOrDefault(); CheckInLocation selectedLocation = selectedGroup.Locations.Where(l => l.Selected && l.Location.Id == locationId && l.Schedules.Any(s => s.Schedule.Id == scheduleId)).FirstOrDefault(); CheckInSchedule selectedSchedule = selectedLocation.Schedules.Where(s => s.Selected && s.Schedule.Id == scheduleId).FirstOrDefault(); selectedSchedule.Selected = false; selectedSchedule.PreSelected = false; // clear checkin rows without anything selected if (!selectedLocation.Schedules.Any(s => s.Selected)) { selectedLocation.Selected = false; selectedLocation.PreSelected = false; } if (!selectedGroup.Locations.Any(l => l.Selected)) { selectedGroup.Selected = false; selectedGroup.PreSelected = false; } if (!selectedGroups.Any()) { selectedPerson.Selected = false; selectedPerson.PreSelected = false; } BindGrid(); }
/// <summary> /// Binds the locations. /// </summary> /// <param name="person">The person.</param> protected void BindLocations(List <CheckInGroupType> groupTypes) { int groupTypeId = ViewState["groupTypeId"].ToString().AsType <int>(); int locationId = ViewState["locationId"].ToString().AsType <int>(); CheckInGroupType groupType = null; if (groupTypes.Any(gt => gt.GroupType.Id == groupTypeId)) { groupType = groupTypes.Where(gt => gt.GroupType.Id == groupTypeId).FirstOrDefault(); } else { groupType = groupTypes.FirstOrDefault(); } CheckInLocation location = null; var locations = groupType.Groups.SelectMany(g => g.Locations).ToList(); if (locationId > 0) { location = locations.Where(l => l.Location.Id == locationId).FirstOrDefault(); var selectedLocationPlaceInList = locations.IndexOf(location) + 1; var pageSize = this.Pager.PageSize; var pageToGoTo = selectedLocationPlaceInList / pageSize; if (selectedLocationPlaceInList % pageSize != 0) { pageToGoTo++; } this.Pager.SetPageProperties((pageToGoTo - 1) * this.Pager.PageSize, this.Pager.MaximumRows, false); } else { location = locations.FirstOrDefault(); } Session["locations"] = locations; lvLocation.DataSource = locations; lvLocation.DataBind(); pnlLocations.Update(); }
/// <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, WorkflowAction action, Object entity, out List <string> errorMessages) { var checkInState = GetCheckInState(entity, out errorMessages); if (checkInState != null) { bool loadAll = GetAttributeValue(action, "LoadAll").AsBoolean(); foreach (var family in checkInState.CheckIn.GetFamilies(true)) { foreach (var person in family.GetPeople(!loadAll)) { foreach (var groupType in person.GetGroupTypes(!loadAll).ToList()) { var kioskGroupType = checkInState.Kiosk.ActiveGroupTypes(checkInState.ConfiguredGroupTypes) .Where(g => g.GroupType.Id == groupType.GroupType.Id) .FirstOrDefault(); if (kioskGroupType != null) { foreach (var group in groupType.GetGroups(!loadAll)) { var closedGroupLocationIds = new AttendanceOccurrenceService(rockContext) .Queryable() .AsNoTracking() .Where(o => o.GroupId == group.Group.Id && o.OccurrenceDate == RockDateTime.Today) .WhereAttributeValue(rockContext, "rocks.kfs.OccurrenceClosed", "True") .Select(l => l.LocationId) .ToList(); var loadBalance = group.Group.GetAttributeValue("rocks.kfs.LoadBalanceLocations").AsBoolean(); if (loadBalance && loadAll) { group.Locations.Clear(); } var locationAttendance = new Dictionary <CheckInLocation, int>(); foreach (var kioskGroup in kioskGroupType.KioskGroups .Where(g => g.Group.Id == group.Group.Id && g.IsCheckInActive) .ToList()) { foreach (var kioskLocation in kioskGroup.KioskLocations.Where(l => l.IsCheckInActive && l.IsActiveAndNotFull && !closedGroupLocationIds.Contains(l.Location.Id))) { if (!group.Locations.Any(l => l.Location.Id == kioskLocation.Location.Id)) { var checkInLocation = new CheckInLocation(); checkInLocation.Location = kioskLocation.Location.Clone(false); checkInLocation.Location.CopyAttributesFrom(kioskLocation.Location); checkInLocation.CampusId = kioskLocation.CampusId; checkInLocation.Order = kioskLocation.Order; locationAttendance.Add(checkInLocation, KioskLocationAttendance.Get(checkInLocation.Location.Id).CurrentCount); } } } if (loadBalance) { var sortedLocationAttendance = locationAttendance.ToList(); sortedLocationAttendance.Sort((x, y) => x.Key.Location.Name.CompareTo(y.Key.Location.Name)); sortedLocationAttendance.Sort((x, y) => x.Value.CompareTo(y.Value)); var order = 0; foreach (var checkInLocationPair in sortedLocationAttendance) { var checkInLocation = checkInLocationPair.Key; checkInLocation.Order = order; group.Locations.Add(checkInLocation); order++; } } else { group.Locations.AddRange(locationAttendance.Select(l => l.Key).ToList()); } } } } } } return(true); } return(false); }
protected override void OnLoad(EventArgs e) { base.OnLoad(e); RockPage.AddScriptLink("~/Scripts/CheckinClient/checkin-core.js"); if (CurrentWorkflow == null || CurrentCheckInState == null) { NavigateToHomePage(); } else { if (!Page.IsPostBack) { ClearSelection(); var personSchedules = new List <CheckInSchedule>(); var distinctSchedules = new List <CheckInSchedule>(); if (CurrentCheckInType != null && CurrentCheckInType.TypeOfCheckin == TypeOfCheckin.Family) { CheckInFamily family = CurrentCheckInState.CheckIn.CurrentFamily; if (family != null) { foreach (var schedule in family.GetPeople(true).SelectMany(p => p.PossibleSchedules).ToList()) { personSchedules.Add(schedule); if (!distinctSchedules.Any(s => s.Schedule.Id == schedule.Schedule.Id)) { distinctSchedules.Add(schedule); } } } else { GoBack(); } lTitle.Text = GetTitleText(); lbSelect.Text = "Next"; lbSelect.Attributes.Add("data-loading-text", "Loading..."); } else { CheckInPerson person = CurrentCheckInState.CheckIn.Families.Where(f => f.Selected).SelectMany(f => f.People.Where(p => p.Selected)).FirstOrDefault(); CheckInGroup group = null; CheckInLocation location = null; if (person != null) { group = person.GroupTypes.Where(t => t.Selected).SelectMany(t => t.Groups.Where(g => g.Selected)).FirstOrDefault(); if (group != null) { location = group.Locations.Where(l => l.Selected).FirstOrDefault(); } } if (location == null) { GoBack(); } lTitle.Text = GetTitleText(); lbSelect.Text = "Check In"; lbSelect.Attributes.Add("data-loading-text", "Printing..."); personSchedules = location.Schedules.Where(s => !s.ExcludedByFilter).ToList(); distinctSchedules = personSchedules; } lCaption.Text = GetAttributeValue(AttributeKey.Caption); if (distinctSchedules.Count == 1) { personSchedules.ForEach(s => s.Selected = true); ProcessSelection(maWarning); } else { string script = string.Format(@" <script> function GetTimeSelection() {{ var ids = ''; $('div.checkin-timelist button.active').each( function() {{ ids += $(this).attr('schedule-id') + ','; }}); if (ids == '') {{ bootbox.alert('Please select at least one time'); return false; }} else {{ $('#{0}').button('loading') $('#{1}').val(ids); return true; }} }} </script> ", lbSelect.ClientID, hfTimes.ClientID); Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "SelectTime", script); rSelection.DataSource = distinctSchedules .OrderBy(s => s.StartTime.Value.TimeOfDay) .ThenBy(s => s.Schedule.Name) .ToList(); rSelection.DataBind(); } } } }
/// <summary> /// Enforces the strict location threshold by removing attendances that would have ended up going into full location+schedules. /// Note: The is also checked earlier in the check-in process, so this catches ones that might have just gotten full in the last few seconds. /// </summary> /// <param name="action">The action.</param> /// <param name="checkInState">State of the check in.</param> /// <param name="attendanceService">The attendance service.</param> /// <param name="currentOccurrences">The current occurrences.</param> /// <param name="person">The person.</param> /// <param name="group">The group.</param> /// <param name="location">The location.</param> /// <param name="schedule">The schedule.</param> /// <param name="startDateTime">The start date time.</param> private void EnforceStrictLocationThreshold(WorkflowAction action, CheckInState checkInState, AttendanceService attendanceService, List <OccurrenceRecord> currentOccurrences, CheckInPerson person, CheckInGroup group, CheckInLocation location, CheckInSchedule schedule, DateTime startDateTime) { var thresHold = location.Location.SoftRoomThreshold.Value; if (checkInState.ManagerLoggedIn && location.Location.FirmRoomThreshold.HasValue && location.Location.FirmRoomThreshold.Value > location.Location.SoftRoomThreshold.Value) { thresHold = location.Location.FirmRoomThreshold.Value; } var currentOccurrence = GetCurrentOccurrence(currentOccurrences, location, schedule, startDateTime.Date); // The totalAttended is the number of people still checked in (not people who have been checked-out) // not counting the current person who may already be checked in, // + the number of people we have checked in so far (but haven't been saved yet). var attendanceQry = attendanceService.GetByDateOnLocationAndSchedule(startDateTime.Date, location.Location.Id, schedule.Schedule.Id) .AsNoTracking() .Where(a => a.EndDateTime == null); // Only process if the current person is NOT already checked-in to this location and schedule if (!attendanceQry.Where(a => a.PersonAlias.PersonId == person.Person.Id).Any()) { var totalAttended = attendanceQry.Count() + (currentOccurrence == null ? 0 : currentOccurrence.Count); // If over capacity, remove the schedule and add a warning message. if (totalAttended >= thresHold) { // Remove the schedule since the location was full for this schedule. location.Schedules.Remove(schedule); var message = new CheckInMessage() { MessageType = MessageType.Warning }; var mergeFields = Lava.LavaHelper.GetCommonMergeFields(null); mergeFields.Add(MergeFieldKey.Person, person.Person); mergeFields.Add(MergeFieldKey.Group, group.Group); mergeFields.Add(MergeFieldKey.Location, location.Location); mergeFields.Add(MergeFieldKey.Schedule, schedule.Schedule); message.MessageText = GetAttributeValue(action, AttributeKey.NotCheckedInMessageFormat).ResolveMergeFields(mergeFields); // Now add it to the check-in state message list for others to see. checkInState.Messages.Add(message); return; } else { // Keep track of anyone who was checked in so far. if (currentOccurrence == null) { currentOccurrence = new OccurrenceRecord() { Date = startDateTime.Date, LocationId = location.Location.Id, ScheduleId = schedule.Schedule.Id }; currentOccurrences.Add(currentOccurrence); } currentOccurrence.Count += 1; } } }
/// <summary> /// Gets the current occurrence from the given list for the matching location, schedule and startDateTime. /// </summary> /// <param name="currentOccurrences">The current occurrences.</param> /// <param name="location">The location.</param> /// <param name="schedule">The schedule.</param> /// <param name="startDateTime">The start date time.</param> /// <returns></returns> private OccurrenceRecord GetCurrentOccurrence(List <OccurrenceRecord> currentOccurrences, CheckInLocation location, CheckInSchedule schedule, DateTime startDateTime) { return(currentOccurrences .Where(a => a.Date == startDateTime.Date && a.LocationId == location.Location.Id && a.ScheduleId == schedule.Schedule.Id) .FirstOrDefault()); }
/// <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 peopleWithoutAssignments = 0; bool roomBalance = GetAttributeValue(action, "RoomBalance").AsBoolean(); int balanceOverride = GetAttributeValue(action, "DifferentialOverride").AsIntegerOrNull() ?? 10; int previousMonthsNumber = GetAttributeValue(action, "PreviousMonthsAttendance").AsIntegerOrNull() ?? 3; int maxAssignments = GetAttributeValue(action, "MaxAssignments").AsIntegerOrNull() ?? 5; var cutoffDate = Rock.RockDateTime.Today.AddMonths(previousMonthsNumber * -1); var attendanceService = new AttendanceService(rockContext); var family = checkInState.CheckIn.Families.FirstOrDefault(f => f.Selected); if (family != null) { // get the number of people checking in, including visitors or first-timers peopleWithoutAssignments = family.People.Where(p => p.Selected).Count(); foreach (var previousAttender in family.People.Where(p => p.Selected && !p.FirstTime)) { var personGroupTypeIds = previousAttender.GroupTypes.Select(gt => gt.GroupType.Id); var lastDateAttendances = attendanceService.Queryable() .Where(a => a.PersonAlias.PersonId == previousAttender.Person.Id && personGroupTypeIds.Contains(a.Group.GroupTypeId) && a.StartDateTime >= cutoffDate && a.DidAttend == true) .OrderByDescending(a => a.StartDateTime).Take(maxAssignments) .ToList(); if (lastDateAttendances.Any()) { bool createdMatchingAssignment = false; var isSpecialNeeds = previousAttender.Person.GetAttributeValue("IsSpecialNeeds").AsBoolean(); var lastAttended = lastDateAttendances.Max(a => a.StartDateTime).Date; foreach (var groupAttendance in lastDateAttendances.Where(a => a.StartDateTime >= lastAttended)) { // 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 || isSpecialNeeds)); if (groupType != null) { CheckInGroup group = null; if (groupType.Groups.Count == 1) { // Only a single group is open group = groupType.Groups.FirstOrDefault(g => !g.ExcludedByFilter || isSpecialNeeds); } else { // Pick the group they last attended group = groupType.Groups.FirstOrDefault(g => g.Group.Id == groupAttendance.GroupId && (!g.ExcludedByFilter || isSpecialNeeds)); if (group != null && roomBalance && !isSpecialNeeds) { var currentAttendance = group.Locations.Select(l => KioskLocationAttendance.Read(l.Location.Id).CurrentCount).Sum(); var lowestAttendedGroup = groupType.Groups.Where(g => !g.ExcludedByFilter) .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 - balanceOverride)) { 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 || isSpecialNeeds); } else { // Pick the location they last attended location = group.Locations.FirstOrDefault(l => l.Location.Id == groupAttendance.LocationId && (!l.ExcludedByFilter || isSpecialNeeds)); if (location != null && roomBalance && !isSpecialNeeds) { var currentAttendance = KioskLocationAttendance.Read(location.Location.Id).CurrentCount; var lowestAttendedLocation = group.Locations.Where(l => !l.ExcludedByFilter) .Select(l => new { Location = l, Attendance = KioskLocationAttendance.Read(location.Location.Id).CurrentCount }) .OrderBy(l => l.Attendance) .FirstOrDefault(); if (lowestAttendedLocation != null && lowestAttendedLocation.Attendance < (currentAttendance - balanceOverride)) { location = lowestAttendedLocation.Location; } } } if (location != null) { CheckInSchedule schedule = null; if (location.Schedules.Count == 1) { schedule = location.Schedules.FirstOrDefault(s => !s.ExcludedByFilter || isSpecialNeeds); } else if (groupAttendance.ScheduleId != null) { schedule = location.Schedules.FirstOrDefault(s => s.Schedule.Id == groupAttendance.ScheduleId && (!s.ExcludedByFilter || isSpecialNeeds)); } else { // if the schedule doesn't exactly match but everything else does, select it schedule = location.Schedules.FirstOrDefault(s => (!s.ExcludedByFilter && !isSpecialNeeds)); } if (schedule != null) { schedule.Selected = true; schedule.PreSelected = true; schedule.LastCheckIn = groupAttendance.StartDateTime; location.Selected = true; location.PreSelected = true; location.LastCheckIn = groupAttendance.StartDateTime; group.Selected = true; group.PreSelected = true; group.LastCheckIn = groupAttendance.StartDateTime; groupType.Selected = true; groupType.PreSelected = true; group.LastCheckIn = groupAttendance.StartDateTime; groupType.LastCheckIn = groupAttendance.StartDateTime; groupType.Selected = true; groupType.PreSelected = true; previousAttender.PreSelected = true; previousAttender.LastCheckIn = groupAttendance.StartDateTime; createdMatchingAssignment = true; } } } } } if (createdMatchingAssignment) { peopleWithoutAssignments--; } } } } // true condition will continue to the next auto-assignment // false condition will stop processing auto-assignments if (action.Activity.AttributeValues.Any() && action.Activity.AttributeValues.ContainsKey("ContinueAssignments")) { var continueAssignments = peopleWithoutAssignments > 0; action.Activity.AttributeValues["ContinueAssignments"].Value = continueAssignments.ToString(); } 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()).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); } 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); }
/// <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); } bool roomBalance = GetAttributeValue(action, "RoomBalance").AsBoolean(); bool useGroupMembership = GetAttributeValue(action, "PrioritizeGroupMembership").AsBoolean(); int balanceOverride = GetAttributeValue(action, "DifferentialOverride").AsIntegerOrNull() ?? 10; var family = checkInState.CheckIn.Families.FirstOrDefault(f => f.Selected); if (family != null) { // don't run for people who already have attendance assignments foreach (var person in family.People.Where(f => f.Selected && !f.GroupTypes.Any(gt => gt.Selected))) { decimal baseVariance = 100; char[] delimiter = { ',' }; // variable must be a string to compare to group attribute value var specialNeedsValue = person.Person.GetAttributeValue("IsSpecialNeeds").ToStringSafe(); var isSpecialNeeds = specialNeedsValue.AsBoolean(); 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("AgeRange") && g.Group.AttributeValues["AgeRange"].Value != null && g.Group.AttributeValues.ContainsKey("IsSpecialNeeds") == isSpecialNeeds ) .ToList() .Select(g => new { Group = g, AgeRange = g.Group.AttributeValues["AgeRange"].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 (isSpecialNeeds) { closestNeedsGroup = closestAgeGroup; } } } } else if (isSpecialNeeds) { // special needs was checked but no 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("GradeRange") && g.Group.AttributeValues["GradeRange"].Value != null) .ToList() .Select(g => new { Group = g, GradeOffsets = g.Group.AttributeValues["GradeRange"].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: Ability, then Grade, then Age, then 1st available bestGroup = closestNeedsGroup ?? closestGradeGroup ?? closestAgeGroup ?? validGroups.FirstOrDefault(g => !g.ExcludedByFilter); // room balance if they fit into multiple groups if (bestGroup != null && roomBalance) { var currentGroupAttendance = bestGroup.Locations.Select(l => KioskLocationAttendance.Read(l.Location.Id).CurrentCount).Sum(); var lowestGroup = validGroups.Where(g => !g.ExcludedByFilter) .Select(g => new { Group = g, Attendance = g.Locations.Select(l => KioskLocationAttendance.Read(l.Location.Id).CurrentCount).Sum() }) .OrderBy(g => g.Attendance) .FirstOrDefault(); if (lowestGroup != null && lowestGroup.Attendance < (currentGroupAttendance - balanceOverride)) { 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; IEnumerable <CheckInSchedule> validSchedules; if (numValidLocations == 1) { bestLocation = validLocations.FirstOrDefault(); validSchedules = bestLocation.Schedules; } else { var orderedLocations = validLocations.Where(l => !l.ExcludedByFilter && l.Schedules.Any(s => s.Schedule.IsCheckInActive)); if (roomBalance) { orderedLocations = orderedLocations.OrderBy(l => KioskLocationAttendance.Read(l.Location.Id).CurrentCount); } bestLocation = orderedLocations.FirstOrDefault(); validSchedules = bestLocation.Schedules; } // check how many schedules exist without getting the whole list int numValidSchedules = validSchedules.Take(2).Count(); if (numValidSchedules > 0) { var bestSchedule = validSchedules.FirstOrDefault(); 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.PreSelected = true; } } } } } } } } } return(true); }
private CheckInStatus DehydrateStatus(CheckInStatus status) { var checkinstatus = new CheckInStatus { SearchType = status.SearchType, SearchValue = status.SearchValue, Families = new List <CheckInFamily>() }; foreach (var family in status.Families) { var checkinFamily = new CheckInFamily() { Selected = family.Selected, Caption = family.Caption, SubCaption = family.SubCaption, AttendanceIds = family.AttendanceIds, FirstNames = family.FirstNames, Group = new Group() { Id = family.Group.Id }, People = new List <CheckInPerson>() }; checkinstatus.Families.Add(checkinFamily); foreach (var person in family.People) { var checkInPerson = new CheckInPerson { GroupTypes = new List <CheckInGroupType>(), FirstTime = person.FirstTime, FamilyMember = person.FamilyMember, Person = new Person { Id = person.Person.Id }, PreSelected = person.PreSelected, SecurityCode = person.SecurityCode, ExcludedByFilter = person.ExcludedByFilter, Selected = person.Selected }; checkinFamily.People.Add(checkInPerson); foreach (var grouptype in person.GroupTypes) { var checkinGroupType = new CheckInGroupType { GroupType = grouptype.GroupType, Selected = grouptype.Selected, PreSelected = grouptype.PreSelected, ExcludedByFilter = grouptype.ExcludedByFilter, Groups = new List <CheckInGroup>() }; checkInPerson.GroupTypes.Add(checkinGroupType); foreach (var group in grouptype.Groups) { var checkinGroup = new CheckInGroup { Group = new Group { Id = group.Group.Id }, Selected = group.Selected, PreSelected = group.PreSelected, ExcludedByFilter = group.ExcludedByFilter, Locations = new List <CheckInLocation>() }; checkinGroupType.Groups.Add(group); foreach (var location in group.Locations) { var checkinLocation = new CheckInLocation { Location = new Location { Id = location.Location.Id }, Selected = location.Selected, PreSelected = location.PreSelected, ExcludedByFilter = location.ExcludedByFilter, CampusId = location.CampusId, Schedules = new List <CheckInSchedule>() }; checkinGroup.Locations.Add(checkinLocation); foreach (var schedule in location.Schedules) { var checkinSchedule = new CheckInSchedule { Schedule = new Schedule { Id = schedule.Schedule.Id }, Selected = schedule.Selected, PreSelected = schedule.PreSelected, ExcludedByFilter = schedule.ExcludedByFilter, CampusId = schedule.CampusId, StartTime = schedule.StartTime }; checkinLocation.Schedules.Add(checkinSchedule); } } } } } } return(checkinstatus); }
protected override void OnLoad(EventArgs e) { base.OnLoad(e); RockPage.AddScriptLink("~/Scripts/iscroll.js"); RockPage.AddScriptLink("~/Scripts/CheckinClient/checkin-core.js"); if (CurrentWorkflow == null || CurrentCheckInState == null) { NavigateToHomePage(); } else { if (!Page.IsPostBack) { ClearSelection(); CheckInPerson person = null; CheckInGroup group = null; CheckInLocation location = null; person = CurrentCheckInState.CheckIn.Families.Where(f => f.Selected) .SelectMany(f => f.People.Where(p => p.Selected)) .FirstOrDefault(); if (person != null) { group = person.GroupTypes.Where(t => t.Selected) .SelectMany(t => t.Groups.Where(g => g.Selected)) .FirstOrDefault(); if (group != null) { location = group.Locations.Where(l => l.Selected) .FirstOrDefault(); } } if (location == null) { GoBack(); } lTitle.Text = person.ToString(); lSubTitle.Text = string.Format("{0} - {1}", group.ToString(), location.ToString()); var availSchedules = location.Schedules.Where(s => !s.ExcludedByFilter).ToList(); if (availSchedules.Count == 1) { availSchedules.FirstOrDefault().Selected = true; ProcessSelection(maWarning); } else { string script = string.Format(@" <script> function GetTimeSelection() {{ var ids = ''; $('div.checkin-timelist button.active').each( function() {{ ids += $(this).attr('schedule-id') + ','; }}); if (ids == '') {{ bootbox.alert('Please select at least one time'); return false; }} else {{ $('#{0}').button('loading') $('#{1}').val(ids); return true; }} }} </script> ", lbSelect.ClientID, hfTimes.ClientID); Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "SelectTime", script); rSelection.DataSource = availSchedules .OrderBy(s => s.StartTime.Value.TimeOfDay) .ThenBy(s => s.Schedule.Name) .ToList(); rSelection.DataBind(); } } } }
private void SelectLocation(CheckInPerson person, CheckInGroupType gt, CheckInGroup g, CheckInLocation l) { l.Selected = !l.Selected; if (l.Selected) { person.Selected = true; gt.Selected = true; g.Selected = true; foreach (var s in l.Schedules) { s.Selected = true; } } else { foreach (var s in l.Schedules) { s.Selected = false; } if (!g.Locations.Where(_l => _l.Selected).Any()) { g.Selected = false; } if (!gt.Groups.Where(_g => _g.Selected).Any()) { gt.Selected = false; } if (!person.GroupTypes.Where(_gt => _gt.Selected).Any()) { person.Selected = false; } } SaveState(); BuildMemberCards(); }