@page "/calendar/state-schedule-handout" @attribute [Authorize] @using Microsoft.EntityFrameworkCore @using Microsoft.Extensions.Options @using System.Globalization @using WebApp.Models @using WebApp.Utility @using WebApp.Services @inject AppDbContext Context @inject IConfiguration Configuration @inject IOptionsMonitor HandoutOptionsMonitor @inject IEventOccurrenceService EventOccurrenceService
@if (_students == null || _allOccurrences == null) {

Loading...

} else { var opts = HandoutOptionsMonitor.CurrentValue; @foreach (var student in _students) { @if (string.IsNullOrWhiteSpace(student.StateId)) { @student.Name } else { @($"{student.Name} - {student.StateId}") } TSA @_competitionYear @_stateAbbrev State Schedule Events State ID Event Activity @foreach (var eventRow in GetEventSummaryRows(student)) { @eventRow.StateRegistrationId @eventRow.EventName @eventRow.Activity } @{ var scheduleRows = BuildStudentSchedule(student, opts).ToList(); } Schedule @if (scheduleRows.Count == 0) { No schedule entries for imported occurrences. } else { @foreach (var dateGroup in scheduleRows.GroupBy(o => o.StartTime.Date)) { @FormatDateHeading(dateGroup.Key) Time Event Location @foreach (var occ in dateGroup.OrderBy(o => o.StartTime)) { @FormatTimeDisplay(occ) @FormatEventColumn(occ) @(occ.Location ?? "") } } } } Combined schedule Imported occurrences relevant to this chapter. @{ var combinedOccurrences = GetCombinedScheduleOccurrences().ToList(); } @if (combinedOccurrences.Count == 0) { No relevant event occurrences found for your current team registrations. } @foreach (var dateGroup in combinedOccurrences.GroupBy(o => o.StartTime.Date)) { @FormatDateHeading(dateGroup.Key) Time Event Location @foreach (var tlGroup in dateGroup .OrderBy(o => o.StartTime) .GroupBy(o => (FormatTimeDisplay(o), o.Location ?? "")) .Select(g => g.ToList())) { if (tlGroup.Count == 1) { var occ = tlGroup[0]; @FormatTimeDisplay(occ) @FormatCombinedScheduleEventCell(occ) @(occ.Location ?? "") } else { var genericOcc = tlGroup.FirstOrDefault(o => !o.EventDefinitionId.HasValue); var specificOccs = tlGroup .Where(o => o.EventDefinitionId.HasValue) .OrderBy(o => FormatEventColumn(o), StringComparer.OrdinalIgnoreCase) .ToList(); var rowCount = (genericOcc != null ? 1 : 0) + specificOccs.Count; var representative = genericOcc ?? specificOccs[0]; if (genericOcc != null) { @FormatTimeDisplay(representative) @FormatCombinedScheduleEventCell(genericOcc) @(representative.Location ?? "") @foreach (var sub in specificOccs) { @FormatCombinedScheduleEventCell(sub) } } else { @FormatTimeDisplay(representative) @FormatCombinedScheduleEventCell(specificOccs[0]) @(representative.Location ?? "") @foreach (var sub in specificOccs.Skip(1)) { @FormatCombinedScheduleEventCell(sub) } } } } } } @code { private Student[]? _students; private List? _allOccurrences; private Dictionary> _teamsByEventDefinitionId = new(); private string _competitionYear = ""; private string _stateAbbrev = ""; private string? _chapterStateId; protected override async Task OnInitializedAsync() { _competitionYear = Configuration["ChapterSettings:CompetitionYear"] ?? ""; _stateAbbrev = Configuration["ChapterSettings:StateAbbrev"] ?? "ST"; _chapterStateId = Configuration["ChapterSettings:StateId"]; _allOccurrences = await Context.EventOccurrences .AsNoTracking() .Include(eo => eo.EventDefinition) .OrderBy(eo => eo.StartTime) .ToListAsync(); var eventDefIds = _allOccurrences .Where(o => o.EventDefinitionId.HasValue) .Select(o => o.EventDefinitionId!.Value) .Distinct() .ToList(); _teamsByEventDefinitionId = await EventOccurrenceService.GetTeamsByEventDefinitionIdsAsync(eventDefIds); // Tracking required: Include Teams->Students creates a graph cycle (Student–Team–Student) that EF disallows with AsNoTracking(). _students = await Context.Students .Include(s => s.Teams) .ThenInclude(t => t!.Event) .Include(s => s.Teams) .ThenInclude(t => t!.Captain) .Include(s => s.Teams) .ThenInclude(t => t!.Students) .OrderBy(s => s.FirstName) .ThenBy(s => s.LastName) .ToArrayAsync(); } private IEnumerable BuildStudentSchedule(Student student, StateScheduleHandoutOptions opts) { var eventIds = student.Teams.Select(t => t.Event.Id).ToHashSet(); var competition = _allOccurrences! .Where(o => o.EventDefinitionId.HasValue && eventIds.Contains(o.EventDefinitionId.Value)) .Where(o => StateScheduleOccurrenceFilter.IncludeCompetitionOccurrenceForStudent(o, opts)); var special = _allOccurrences! .Where(o => o.EventDefinitionId == null) .Where(o => StateScheduleOccurrenceFilter.IncludeSpecialOccurrenceForStudent(o, student, opts)); return competition .Concat(special) .OrderBy(o => o.StartTime) .DistinctBy(o => (o.StartTime, o.Name ?? "")); } private IEnumerable GetCombinedScheduleOccurrences() { return _allOccurrences! .Where(o => { // Keep chapter-wide/special schedule rows. if (!o.EventDefinitionId.HasValue) return true; // Keep only competition events where this chapter has registered teams. return _teamsByEventDefinitionId.TryGetValue(o.EventDefinitionId.Value, out var teams) && teams.Count > 0; }) .OrderBy(o => o.StartTime); } private IEnumerable GetEventSummaryRows(Student student) { foreach (var team in student.Teams.OrderBy(t => t.Event.Name)) { yield return new EventSummaryRow( StateRegistrationId: FormatStateRegistrationId(team, student), EventName: team.Event.Name, Activity: FormatActivitySummary(team, student)); } } /// /// Team events: chapter ChapterSettings:StateId + (e.g. 12227-1). /// Individual events: competitor's . /// private string FormatStateRegistrationId(Team team, Student student) { if (team.Event.EventFormat == EventFormat.Individual) { return string.IsNullOrWhiteSpace(student.StateId) ? "—" : student.StateId.Trim(); } var chap = _chapterStateId?.Trim(); var ident = team.Identifier?.Trim(); if (string.IsNullOrEmpty(chap) && string.IsNullOrEmpty(ident)) return "—"; // Already a full registration id (e.g. "12227-1" or state id stored on team) if (!string.IsNullOrEmpty(ident)) { if (ident.Contains('-', StringComparison.Ordinal)) return ident; if (!string.IsNullOrEmpty(chap) && ident.StartsWith(chap, StringComparison.Ordinal)) return ident; } if (!string.IsNullOrEmpty(chap) && !string.IsNullOrEmpty(ident)) return $"{chap}-{ident}"; return !string.IsNullOrEmpty(chap) ? chap : ident!; } // Activity line comes from event SemifinalistActivity (interview/presentation limits), not Min/MaxTeamSize. private static string FormatActivitySummary(Team team, Student student) { var parts = new List(); if (team.Captain?.Id == student.Id) parts.Add("(Cpt.)"); if (!string.IsNullOrWhiteSpace(team.Event.SemifinalistActivity)) parts.Add(team.Event.SemifinalistActivity!); return string.Join(" ", parts).Trim(); } private static string FormatDateHeading(DateTime date) => date.ToString("MMMM d, dddd", CultureInfo.GetCultureInfo("en-US")); private static string FormatTimeDisplay(EventOccurrence o) { if (!string.IsNullOrWhiteSpace(o.Time)) return o.Time.Trim(); return o.StartTime.ToString("g", CultureInfo.GetCultureInfo("en-US")); } private static string FormatEventColumn(EventOccurrence o) { if (o.EventDefinition != null) { var ev = !string.IsNullOrWhiteSpace(o.EventDefinition.ShortName) ? o.EventDefinition.ShortName : o.EventDefinition.Name; if (string.IsNullOrWhiteSpace(o.Name)) return ev; if (o.Name.Contains(ev, StringComparison.OrdinalIgnoreCase)) return o.Name.Trim(); return $"{ev} {o.Name}".Trim(); } return string.IsNullOrWhiteSpace(o.Name) ? (o.SpecialEventType ?? "") : o.Name.Trim(); } private string FormatCombinedScheduleEventCell(EventOccurrence occ) { var baseText = FormatEventColumn(occ); if (!occ.EventDefinitionId.HasValue) return baseText; if (!_teamsByEventDefinitionId.TryGetValue(occ.EventDefinitionId.Value, out var teams) || teams.Count == 0) return baseText; var isIndividual = occ.EventDefinition?.EventFormat == EventFormat.Individual; var orderedTeams = teams .OrderBy(t => t, Comparer.Create((a, b) => { var cmp = CombinedScheduleTeamSortOrder(a, b); return cmp != 0 ? cmp : a.Id.CompareTo(b.Id); })) .ToList(); var rosterStrings = orderedTeams .Select(t => FormatCombinedScheduleTeamRoster(t, isIndividual)) .Where(s => !string.IsNullOrWhiteSpace(s)) .ToList(); if (rosterStrings.Count == 0) return baseText; var suffix = rosterStrings.Count == 1 ? rosterStrings[0] : string.Join(" ", rosterStrings.Select(r => $"[{r}]")); return $"{baseText} — {suffix}"; } private static int CombinedScheduleTeamSortOrder(Team a, Team b) { var ka = a.Identifier?.Trim() ?? ""; var kb = b.Identifier?.Trim() ?? ""; if (int.TryParse(ka, out var na) && int.TryParse(kb, out var nb)) return na.CompareTo(nb); return string.Compare(ka, kb, StringComparison.OrdinalIgnoreCase); } private static string FormatCombinedScheduleTeamRoster(Team team, bool isIndividual) { var students = team.Students?.ToList() ?? []; if (students.Count == 0) return ""; if (isIndividual) { var ordered = students.OrderBy(s => s.FirstName, StringComparer.OrdinalIgnoreCase); return string.Join(", ", ordered.Select(s => FormatCombinedScheduleStudentSegment(s, team, isIndividual))); } var cap = team.Captain; var capInRoster = cap != null && students.Exists(s => s.Id == cap.Id); IEnumerable orderedTeam = capInRoster ? students.Where(s => s.Id != cap!.Id).OrderBy(s => s.FirstName, StringComparer.OrdinalIgnoreCase).Prepend(cap!) : students.OrderBy(s => s.FirstName, StringComparer.OrdinalIgnoreCase); return string.Join(", ", orderedTeam.Select(s => FormatCombinedScheduleStudentSegment(s, team, isIndividual))); } private static string FormatCombinedScheduleStudentSegment(Student student, Team team, bool isIndividual) { if (isIndividual) { var sid = student.StateId?.Trim(); return !string.IsNullOrEmpty(sid) ? $"{student.FirstName} ({sid})" : student.FirstName; } var isCpt = team.Captain?.Id == student.Id; return isCpt ? $"{student.FirstName} (Cpt.)" : student.FirstName; } private sealed record EventSummaryRow(string StateRegistrationId, string EventName, string Activity); }