/// <summary> /// Builds the status board. /// </summary> private void BuildStatusBoard() { lGroupStatusTableHTML.Text = string.Empty; int numberOfWeeks = GetSelectedNumberOfWeeks(); List <int> selectedGroupIds = GetSelectedGroupIds(); var rockContext = new RockContext(); var groupsQuery = new GroupService(rockContext).GetByIds(selectedGroupIds).Where(a => a.GroupType.IsSchedulingEnabled == true); nbGroupsWarning.Visible = false; if (!groupsQuery.Any()) { nbGroupsWarning.Text = "Please select at least one group."; nbGroupsWarning.NotificationBoxType = NotificationBoxType.Warning; nbGroupsWarning.Visible = true; return; } var groupLocationService = new GroupLocationService(rockContext); var groupLocationsQuery = groupLocationService.Queryable().Where(a => selectedGroupIds.Contains(a.GroupId) && a.Group.GroupType.IsSchedulingEnabled == true); // get all the schedules that are in use by at least one of the GroupLocations var groupsScheduleList = groupLocationsQuery.SelectMany(a => a.Schedules).Distinct().AsNoTracking().ToList(); if (!groupsScheduleList.Any()) { nbGroupsWarning.Text = "The selected groups don't have any location schedules configured."; nbGroupsWarning.NotificationBoxType = NotificationBoxType.Warning; nbGroupsWarning.Visible = true; return; } // get the next N sundayDates List <DateTime> sundayDateList = GetSundayDateList(numberOfWeeks); // build the list of scheduled times for the next n weeks List <ScheduleOccurrenceDate> scheduleOccurrenceDateList = new List <ScheduleOccurrenceDate>(); var currentDate = RockDateTime.Today; foreach (var sundayDate in sundayDateList) { foreach (var schedule in groupsScheduleList) { var sundayWeekStart = sundayDate.AddDays(-6); // get all the occurrences for the selected week for this scheduled (It could be more than once a week if it is a daily scheduled, or it might not be in the selected week if it is every 2 weeks, etc) var scheduledDateTimeList = schedule.GetScheduledStartTimes(sundayWeekStart, sundayDate.AddDays(1)); foreach (var scheduledDateTime in scheduledDateTimeList) { if (scheduledDateTime >= currentDate) { scheduleOccurrenceDateList.Add(new ScheduleOccurrenceDate { Schedule = schedule, ScheduledDateTime = scheduledDateTime }); } } } } scheduleOccurrenceDateList = scheduleOccurrenceDateList.OrderBy(a => a.ScheduledDateTime).ToList(); var latestOccurrenceDate = sundayDateList.Max(); var scheduledOccurrencesQuery = new AttendanceOccurrenceService(rockContext).Queryable().Where(a => a.GroupId.HasValue && a.LocationId.HasValue && a.ScheduleId.HasValue && selectedGroupIds.Contains(a.GroupId.Value)); scheduledOccurrencesQuery = scheduledOccurrencesQuery.Where(a => a.OccurrenceDate >= currentDate && a.OccurrenceDate <= latestOccurrenceDate); var occurrenceScheduledAttendancesList = scheduledOccurrencesQuery.Select(ao => new { Occurrence = ao, ScheduledAttendees = ao.Attendees.Where(a => a.RequestedToAttend == true || a.ScheduledToAttend == true).Select(a => new { ScheduledPerson = a.PersonAlias.Person, a.RequestedToAttend, a.ScheduledToAttend, a.RSVP }) }).ToList(); StringBuilder sbTable = new StringBuilder(); // start of table sbTable.AppendLine("<table class='table schedule-status-board js-schedule-status-board'>"); sbTable.AppendLine("<thead class='schedule-status-board-header js-schedule-status-board-header'>"); sbTable.AppendLine("<tr>"); var tableHeaderLavaTemplate = @" {%- comment -%}empty column for group/location names{%- endcomment -%} <th scope='col'></th> {% for scheduleOccurrenceDate in ScheduleOccurrenceDateList %} <th scope='col'> <span class='date'>{{ scheduleOccurrenceDate.ScheduledDateTime | Date:'MMM d, yyyy' }}</span> <br /> <span class='day-time'>{{ scheduleOccurrenceDate.Schedule.Name }}</span> </th> {% endfor %} "; var headerMergeFields = new Dictionary <string, object> { { "ScheduleOccurrenceDateList", scheduleOccurrenceDateList } }; string tableHeaderHtml = tableHeaderLavaTemplate.ResolveMergeFields(headerMergeFields); sbTable.Append(tableHeaderHtml); sbTable.AppendLine("</tr>"); sbTable.AppendLine("</thead>"); var groupLocationsList = groupsQuery.Where(g => g.GroupLocations.Any()).OrderBy(a => a.Order).ThenBy(a => a.Name).Select(g => new { Group = g, LocationScheduleCapacitiesList = g.GroupLocations.OrderBy(gl => gl.Order).ThenBy(gl => gl.Location.Name).Select(a => new { ScheduleCapacitiesList = a.GroupLocationScheduleConfigs.Select(sc => new ScheduleCapacities { ScheduleId = sc.ScheduleId, MinimumCapacity = sc.MinimumCapacity, DesiredCapacity = sc.DesiredCapacity, MaximumCapacity = sc.MaximumCapacity }), a.Location }).ToList() }).ToList(); var columnsCount = scheduleOccurrenceDateList.Count() + 1; foreach (var groupLocations in groupLocationsList) { var group = groupLocations.Group; StringBuilder sbGroupLocations = new StringBuilder(); sbGroupLocations.AppendLine(string.Format("<tbody class='group-locations js-group-locations' data-group-id='{0}' data-locations-expanded='1'>", group.Id)); // group header row sbGroupLocations.AppendLine("<tr class='group-heading js-group-header thead-dark clickable' >"); sbGroupLocations.AppendLine(string.Format("<th></th><th colspan='{0}'><i class='fa fa-chevron-down'></i> {1}</th>", columnsCount - 1, group.Name)); sbGroupLocations.AppendLine("</tr>"); // group/schedule+locations var locationScheduleCapacitiesList = groupLocations.LocationScheduleCapacitiesList; foreach (var locationScheduleCapacities in locationScheduleCapacitiesList) { var location = locationScheduleCapacities.Location; var scheduleCapacitiesLookup = locationScheduleCapacities.ScheduleCapacitiesList.ToDictionary(k => k.ScheduleId, v => v); sbGroupLocations.AppendLine("<tr class='location-row js-location-row'>"); sbGroupLocations.AppendLine(string.Format("<td class='location' scope='row' data-location-id='{0}'><div>{1}</div></td>", location.Id, location.Name)); foreach (var scheduleOccurrenceDate in scheduleOccurrenceDateList) { var capacities = scheduleCapacitiesLookup.GetValueOrNull(scheduleOccurrenceDate.Schedule.Id) ?? new ScheduleCapacities(); var scheduleLocationStatusHtmlFormat = @"<ul class='location-scheduled-list' data-capacity-min='{1}' data-capacity-desired='{2}' data-capacity-max='{3}' data-scheduled-count='{4}'> {0} </ul>"; StringBuilder sbScheduledListHtml = new StringBuilder(); var occurrenceScheduledAttendances = occurrenceScheduledAttendancesList .FirstOrDefault(ao => ao.Occurrence.OccurrenceDate == scheduleOccurrenceDate.OccurrenceDate && ao.Occurrence.GroupId == groupLocations.Group.Id && ao.Occurrence.ScheduleId == scheduleOccurrenceDate.Schedule.Id && ao.Occurrence.LocationId == location.Id); int scheduledCount = 0; if (occurrenceScheduledAttendances != null && occurrenceScheduledAttendances.ScheduledAttendees.Any()) { // sort so that it is Yes, then Pending, then Denied var scheduledPersonList = occurrenceScheduledAttendances .ScheduledAttendees .OrderBy(a => a.RSVP == RSVP.Yes ? 0 : 1) .ThenBy(a => (a.RSVP == RSVP.Maybe || a.RSVP == RSVP.Unknown) ? 0 : 1) .ThenBy(a => a.RSVP == RSVP.No ? 0 : 1) .ThenBy(a => a.ScheduledPerson.LastName) .ToList(); foreach (var scheduledPerson in scheduledPersonList) { ScheduledAttendanceItemStatus status = ScheduledAttendanceItemStatus.Pending; if (scheduledPerson.RSVP == RSVP.No) { status = ScheduledAttendanceItemStatus.Declined; } else if (scheduledPerson.ScheduledToAttend == true) { status = ScheduledAttendanceItemStatus.Confirmed; } sbScheduledListHtml.AppendLine(string.Format("<li class='slot person {0}' data-status='{0}'><i class='status-icon'></i><span class='person-name'>{1}</span></li>", status.ConvertToString(false).ToLower(), scheduledPerson.ScheduledPerson)); } scheduledCount = scheduledPersonList.Where(a => a.RSVP != RSVP.No).Count(); } if (capacities.DesiredCapacity.HasValue && scheduledCount < capacities.DesiredCapacity.Value) { var countNeeded = capacities.DesiredCapacity.Value - scheduledCount; sbScheduledListHtml.AppendLine(string.Format("<li class='slot persons-needed empty-slot'>{0} {1} needed</li>", countNeeded, "person".PluralizeIf(countNeeded != 1))); // add empty slots if we are under the desired count (not including the slot for the 'persons-needed' li) var emptySlotsToAdd = countNeeded - 1; while (emptySlotsToAdd > 0) { sbScheduledListHtml.AppendLine("<li class='slot empty-slot'></li>"); emptySlotsToAdd--; } } var scheduledLocationsStatusHtml = string.Format(scheduleLocationStatusHtmlFormat, sbScheduledListHtml, capacities.MinimumCapacity, capacities.DesiredCapacity, capacities.MaximumCapacity, scheduledCount); sbGroupLocations.AppendLine(string.Format("<td class='schedule-location js-schedule-location' data-schedule-id='{0}'><div>{1}</div></td>", scheduleOccurrenceDate.Schedule.Id, scheduledLocationsStatusHtml)); } sbGroupLocations.AppendLine("</tr>"); } sbGroupLocations.AppendLine("</tbody>"); sbTable.Append(sbGroupLocations.ToString()); } // closing divs for main header sbTable.AppendLine("</tr>"); sbTable.AppendLine("</thead>"); // closing divs for table sbTable.AppendLine("</table>"); lGroupStatusTableHTML.Text = sbTable.ToString(); }
/// <summary> /// Builds the status board. /// </summary> private void BuildStatusBoard() { lGroupStatusTableHTML.Text = string.Empty; int numberOfWeeks = GetSelectedNumberOfWeeks(); var selectedGroupIds = new List <int>(); var groupsWarningText = "Please select at least one group."; var rockContext = new RockContext(); var groupService = new GroupService(rockContext); var group = GetGroupFromParameter(groupService); if (group != null) { // If a Group Guid was passed in, make sure the user has permission to schedule the // group (and that scheduling is enabled), and put only that group in the // selectedGroupIds list. bool isAuthorized = group.IsAuthorized(Authorization.SCHEDULE, CurrentPerson); if (!isAuthorized) { groupsWarningText = $"You are not authorized to schedule this group."; } else if (!group.GroupType.IsSchedulingEnabled) { groupsWarningText = $"Scheduling is not enabled for this group type ({group.GroupType.Name})."; } else if (group.DisableScheduling) { groupsWarningText = $"Scheduling is disabled for this group ({group.Name})."; } else { selectedGroupIds = new List <int> { group.Id }; } } else { selectedGroupIds = GetSelectedGroupIds(); } var groupsQuery = groupService .GetByIds(selectedGroupIds) .Where(a => a.GroupType.IsSchedulingEnabled == true && a.DisableScheduling == false); nbGroupsWarning.Visible = false; if (!groupsQuery.Any()) { nbGroupsWarning.Text = groupsWarningText; nbGroupsWarning.NotificationBoxType = NotificationBoxType.Warning; nbGroupsWarning.Visible = true; return; } var groupLocationService = new GroupLocationService(rockContext); var groupLocationsQuery = groupLocationService.Queryable().Where(a => selectedGroupIds.Contains(a.GroupId) && a.Group.GroupType.IsSchedulingEnabled == true); // get all the schedules that are in use by at least one of the GroupLocations var groupsScheduleList = groupLocationsQuery.SelectMany(a => a.Schedules).Where(s => s.IsActive).Distinct().AsNoTracking().ToList(); if (!groupsScheduleList.Any()) { nbGroupsWarning.Text = "The selected groups don't have any location schedules configured."; nbGroupsWarning.NotificationBoxType = NotificationBoxType.Warning; nbGroupsWarning.Visible = true; return; } // get the next N sundayDates List <DateTime> sundayDateList = GetSundayDateList(numberOfWeeks); // build the list of scheduled times for the next n weeks List <ScheduleOccurrenceDate> scheduleOccurrenceDateList = new List <ScheduleOccurrenceDate>(); var currentDate = RockDateTime.Today; foreach (var sundayDate in sundayDateList) { foreach (var schedule in groupsScheduleList) { var sundayWeekStart = sundayDate.AddDays(-6); // get all the occurrences for the selected week for this scheduled (It could be more than once a week if it is a daily scheduled, or it might not be in the selected week if it is every 2 weeks, etc) var scheduledDateTimeList = schedule.GetScheduledStartTimes(sundayWeekStart, sundayDate.AddDays(1)); foreach (var scheduledDateTime in scheduledDateTimeList) { if (scheduledDateTime >= currentDate) { scheduleOccurrenceDateList.Add(new ScheduleOccurrenceDate { Schedule = schedule, ScheduledDateTime = scheduledDateTime }); } } } } scheduleOccurrenceDateList = scheduleOccurrenceDateList.OrderBy(a => a.ScheduledDateTime).ToList(); var latestOccurrenceDate = sundayDateList.Max(); var scheduledOccurrencesQuery = new AttendanceOccurrenceService(rockContext).Queryable().Where(a => a.GroupId.HasValue && a.LocationId.HasValue && a.ScheduleId.HasValue && selectedGroupIds.Contains(a.GroupId.Value)); scheduledOccurrencesQuery = scheduledOccurrencesQuery.Where(a => a.OccurrenceDate >= currentDate && a.OccurrenceDate <= latestOccurrenceDate); var occurrenceScheduledAttendancesList = scheduledOccurrencesQuery.Select(ao => new ScheduledAttendanceInfo { Occurrence = ao, ScheduledAttendees = ao.Attendees.Where(a => a.RequestedToAttend == true || a.ScheduledToAttend == true).Select(a => new ScheduledPersonInfo { ScheduledPerson = a.PersonAlias.Person, RequestedToAttend = a.RequestedToAttend, ScheduledToAttend = a.ScheduledToAttend, RSVP = a.RSVP, DeclineReasonValueId = a.DeclineReasonValueId }).ToList() }).ToList(); StringBuilder sbTable = new StringBuilder(); // start of table sbTable.AppendLine("<table class='table schedule-status-board js-schedule-status-board'>"); sbTable.AppendLine("<thead class='schedule-status-board-header js-schedule-status-board-header'>"); sbTable.AppendLine("<tr>"); var tableHeaderLavaTemplate = @" {%- comment -%}empty column for group/location names{%- endcomment -%} <th scope='col'></th> {% for scheduleOccurrenceDate in ScheduleOccurrenceDateList %} <th scope='col'> <span class='date'>{{ scheduleOccurrenceDate.ScheduledDateTime | Date:'MMM d, yyyy' }}</span> <br /> <span class='day-time'>{{ scheduleOccurrenceDate.Schedule.Name }}</span> </th> {% endfor %} "; var headerMergeFields = new Dictionary <string, object> { { "ScheduleOccurrenceDateList", scheduleOccurrenceDateList } }; string tableHeaderHtml = tableHeaderLavaTemplate.ResolveMergeFields(headerMergeFields); sbTable.Append(tableHeaderHtml); sbTable.AppendLine("</tr>"); sbTable.AppendLine("</thead>"); var groupLocationsList = groupsQuery.Where(g => g.GroupLocations.Any()).OrderBy(a => a.Order).ThenBy(a => a.Name).Select(g => new GroupInfo { Group = g, MemberList = g.Members.Select(m => new MemberInfo { PersonId = m.PersonId, GroupRoleId = m.GroupRoleId }).ToList(), // We are currently showing active and inactive locations (not filtering by gl => gl.Location.IsActive). // A room may be closed due to capacity but it will continue to show on the status board. LocationScheduleCapacitiesList = g.GroupLocations.OrderBy(gl => gl.Order).ThenBy(gl => gl.Location.Name).Select(a => new LocationScheduleCapacityInfo { ScheduleCapacitiesList = a.GroupLocationScheduleConfigs.Select(sc => new ScheduleCapacities { ScheduleId = sc.ScheduleId, MinimumCapacity = sc.MinimumCapacity, DesiredCapacity = sc.DesiredCapacity, MaximumCapacity = sc.MaximumCapacity }), Location = a.Location }).ToList() }).ToList(); var columnsCount = scheduleOccurrenceDateList.Count() + 1; foreach (var groupLocations in groupLocationsList) { var locationGroup = groupLocations.Group; var groupType = GroupTypeCache.Get(groupLocations.Group.GroupTypeId); StringBuilder sbGroupLocations = new StringBuilder(); sbGroupLocations.AppendLine(string.Format("<tbody class='group-locations js-group-locations' data-group-id='{0}' data-locations-expanded='1'>", locationGroup.Id)); var groupSchedulingUrl = ResolveRockUrl(string.Format("~/GroupScheduler/{0}", locationGroup.Id)); // group header row sbGroupLocations.AppendLine("<tr class='group-heading js-group-header thead-dark clickable' >"); sbGroupLocations.AppendLine( string.Format( @" <th></th> <th colspan='{0}'> <i class='fa fa-chevron-down js-toggle-panel'></i> {1} <a href='{2}' class='ml-1 text-color js-group-scheduler-link'><i class='{3}'></i></a> </th>", columnsCount - 1, // {0} locationGroup.Name, // {1} groupSchedulingUrl, // {2} "fa fa-calendar-check-o") // {3} ); sbGroupLocations.AppendLine("</tr>"); // group/schedule+locations var locationScheduleCapacitiesList = groupLocations.LocationScheduleCapacitiesList; foreach (var locationScheduleCapacities in locationScheduleCapacitiesList) { var location = locationScheduleCapacities.Location; var scheduleCapacitiesLookup = locationScheduleCapacities.ScheduleCapacitiesList.ToDictionary(k => k.ScheduleId, v => v); sbGroupLocations.AppendLine("<tr class='location-row js-location-row'>"); sbGroupLocations.AppendLine(string.Format("<td class='location' scope='row' data-location-id='{0}'><div>{1}</div></td>", location.Id, location.Name)); foreach (var scheduleOccurrenceDate in scheduleOccurrenceDateList) { var capacities = scheduleCapacitiesLookup.GetValueOrNull(scheduleOccurrenceDate.Schedule.Id) ?? new ScheduleCapacities(); var scheduleLocationStatusHtmlFormat = @"<ul class='location-scheduled-list' data-capacity-min='{1}' data-capacity-desired='{2}' data-capacity-max='{3}' data-scheduled-count='{4}'> {0} </ul>"; StringBuilder sbScheduledListHtml = new StringBuilder(); ScheduledAttendanceInfo occurrenceScheduledAttendances = occurrenceScheduledAttendancesList .FirstOrDefault(ao => ao.Occurrence.OccurrenceDate == scheduleOccurrenceDate.OccurrenceDate && ao.Occurrence.GroupId == groupLocations.Group.Id && ao.Occurrence.ScheduleId == scheduleOccurrenceDate.Schedule.Id && ao.Occurrence.LocationId == location.Id); int scheduledCount = 0; var groupTypeRoleLookup = groupType.Roles.ToDictionary(k => k.Id, v => v); if (occurrenceScheduledAttendances != null && occurrenceScheduledAttendances.ScheduledAttendees.Any()) { foreach (ScheduledPersonInfo scheduledPersonInfo in occurrenceScheduledAttendances.ScheduledAttendees) { var personRolesInGroup = groupLocations .MemberList .Where(m => m.PersonId == scheduledPersonInfo.ScheduledPerson.Id) .Select(m => groupTypeRoleLookup.GetValueOrNull(m.GroupRoleId)) .Where(r => r != null) .ToList(); var personRoleInGroup = personRolesInGroup .OrderBy(r => r.Order) .FirstOrDefault(); scheduledPersonInfo.PersonRoleInGroup = personRoleInGroup; } // sort so that it is Yes, then Pending, then Denied var attendanceScheduledPersonList = occurrenceScheduledAttendances .ScheduledAttendees .OrderBy(a => a.RSVP == RSVP.Yes ? 0 : 1) .ThenBy(a => (a.RSVP == RSVP.Maybe || a.RSVP == RSVP.Unknown) ? 0 : 1) .ThenBy(a => a.RSVP == RSVP.No ? 0 : 1) .ThenBy(a => a.GroupTypeRoleOrder) .ThenBy(a => a.ScheduledPerson.LastName) .ToList(); foreach (ScheduledPersonInfo scheduledPerson in attendanceScheduledPersonList) { ScheduledAttendanceItemStatus status = ScheduledAttendanceItemStatus.Pending; if (scheduledPerson.RSVP == RSVP.No) { status = ScheduledAttendanceItemStatus.Declined; } else if (scheduledPerson.ScheduledToAttend == true) { status = ScheduledAttendanceItemStatus.Confirmed; } var statusAttributes = " class='status-icon'"; if (!string.IsNullOrWhiteSpace(scheduledPerson.DeclineReason)) { statusAttributes = string.Format(" data-original-title='{0}' class='status-icon js-declined-tooltip'", scheduledPerson.DeclineReason); } sbScheduledListHtml.AppendLine( string.Format( "<li class='slot person {0}' data-status='{0}'><i{3}></i><span class='person-name'>{1}</span><span class='person-group-role pull-right'>{2}</span></li>", status.ConvertToString(false).ToLower(), scheduledPerson.ScheduledPerson, scheduledPerson.PersonRoleInGroup, statusAttributes)); } scheduledCount = attendanceScheduledPersonList.Where(a => a.RSVP != RSVP.No).Count(); } if (capacities.DesiredCapacity.HasValue && scheduledCount < capacities.DesiredCapacity.Value) { var countNeeded = capacities.DesiredCapacity.Value - scheduledCount; sbScheduledListHtml.AppendLine(string.Format("<li class='slot persons-needed empty-slot'>{0} {1} needed</li>", countNeeded, "person".PluralizeIf(countNeeded != 1))); // add empty slots if we are under the desired count (not including the slot for the 'persons-needed' li) var emptySlotsToAdd = countNeeded - 1; while (emptySlotsToAdd > 0) { sbScheduledListHtml.AppendLine("<li class='slot empty-slot'></li>"); emptySlotsToAdd--; } } var scheduledLocationsStatusHtml = string.Format(scheduleLocationStatusHtmlFormat, sbScheduledListHtml, capacities.MinimumCapacity, capacities.DesiredCapacity, capacities.MaximumCapacity, scheduledCount); sbGroupLocations.AppendLine(string.Format("<td class='schedule-location js-schedule-location' data-schedule-id='{0}'><div>{1}</div></td>", scheduleOccurrenceDate.Schedule.Id, scheduledLocationsStatusHtml)); } sbGroupLocations.AppendLine("</tr>"); } sbGroupLocations.AppendLine("</tbody>"); sbTable.Append(sbGroupLocations.ToString()); } // closing divs for main header sbTable.AppendLine("</tr>"); sbTable.AppendLine("</thead>"); // closing divs for table sbTable.AppendLine("</table>"); lGroupStatusTableHTML.Text = sbTable.ToString(); }