Exemple #1
0
        /// <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();
        }
Exemple #2
0
        /// <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();
        }