diff --git a/WebApp/Components/Features/Events/Index.razor b/WebApp/Components/Features/Events/Index.razor index 101ea54..2909070 100644 --- a/WebApp/Components/Features/Events/Index.razor +++ b/WebApp/Components/Features/Events/Index.razor @@ -52,19 +52,18 @@ - - [@context.Item.MinTeamSize - @context.Item.MaxTeamSize] + [@context.Item.MinTeamSize - @context.Item.MaxTeamSize] - - - + + + diff --git a/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor b/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor index 618cf61..31dfca4 100644 --- a/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor +++ b/WebApp/Components/Features/MeetingSchedule/ScheduledTeamsList.razor @@ -1,8 +1,9 @@ @using Core.Calculation +@using WebApp.Models @TimeSlotName - @foreach (var team in Teams.OrderBy(e => e.ToString())) + @foreach (var team in Teams.OrderByEventFormatFirst().ThenBy(e => e.ToString())) { var removed = !ScheduledTeams.Contains(team); diff --git a/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor b/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor index 3439d2d..32f3709 100644 --- a/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor +++ b/WebApp/Components/Features/Teams/Components/TeamToggleSelector.razor @@ -1,3 +1,5 @@ +@using WebApp.Models + @if (Title != null) { @Title @@ -9,7 +11,7 @@ ValuesChanged="@OnSelectedTeamsChanged" Vertical="true" CheckMark> - @foreach (var team in Teams.OrderBy(e => e.Event.Name)) + @foreach (var team in Teams.OrderByEventFormatFirst().ThenBy(e => e.Event.Name)) { diff --git a/WebApp/Components/Features/Teams/Index.razor b/WebApp/Components/Features/Teams/Index.razor index 9494586..d542039 100644 --- a/WebApp/Components/Features/Teams/Index.razor +++ b/WebApp/Components/Features/Teams/Index.razor @@ -1,5 +1,6 @@ @using Microsoft.EntityFrameworkCore @using WebApp.Components.Shared.Components +@using WebApp.Models @page "/teams" @attribute [Authorize] @inject AppDbContext Context @@ -9,9 +10,14 @@ Create New - Assignment Printout Handout + + @(_showRegionalOnly ? "Showing Regional Only" : "Show Regional Only") + @@ -20,7 +26,7 @@ ServerData="ServerReload" @ref="_dataGrid" Filterable="true" - RowsPerPage="35" + RowsPerPage="50" Dense="true" Striped="true" Hover="true" @@ -67,24 +73,99 @@ @code { MudDataGrid _dataGrid = null!; private bool _isLoading = true; + private bool _showRegionalOnly = false; + + private async Task ToggleRegionalFilter() + { + try + { + _showRegionalOnly = !_showRegionalOnly; + if (_dataGrid != null) + { + await _dataGrid.ReloadServerData(); + } + } + catch (Exception ex) + { + Snackbar.Add($"Error applying filter: {ex.Message}", Severity.Error); + } + } private async Task> ServerReload(GridState state) { _isLoading = true; try { - var query + IQueryable query = Context.Teams .Include(e => e.Event) .Include(e => e.Students) - .ThenInclude(e => e.EventRankings) - .OrderBy(e => e.Event.Name) - .ThenBy(e => e.Identifier) - .Where(state.FilterDefinitions) - .OrderBy(state.SortDefinitions); + .ThenInclude(e => e.EventRankings); - var totalItems = await query.CountAsync(); - var pagedData = await query.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArrayAsync(); + // Apply regional filter if enabled + // Note: RegionalEvent is a computed property, so we must use the underlying property + if (_showRegionalOnly) + { + query = query.Where(t => t.Event.ChapterEligibilityCountRegionals > 0); + } + + // Apply grid filter definitions + query = query.Where(state.FilterDefinitions); + + // Load all data first + var allTeams = await query.ToArrayAsync(); + + // Always sort by EventFormat FIRST to separate group/individual teams + // Sort in memory to ensure this ordering is maintained regardless of user sorts + var sortedTeams = allTeams.OrderByEventFormatFirst(); + + // Apply user sort definitions as secondary sorts (within each group) + bool appliedUserSort = false; + if (state.SortDefinitions != null && state.SortDefinitions.Any()) + { + var sortDef = state.SortDefinitions.First(); + var sortBy = sortDef.SortBy ?? ""; + + // Handle Event.Name sorting + if (sortBy == "Event.Name" || (sortBy.Contains("Event") && sortBy.Contains("Name"))) + { + sortedTeams = sortDef.Descending + ? sortedTeams.ThenByDescending(t => t.Event.Name) + : sortedTeams.ThenBy(t => t.Event.Name); + appliedUserSort = true; + } + // Handle Identifier sorting + else if (sortBy == "Identifier") + { + sortedTeams = sortDef.Descending + ? sortedTeams.ThenByDescending(t => t.Identifier ?? "") + : sortedTeams.ThenBy(t => t.Identifier ?? ""); + appliedUserSort = true; + } + } + + // Apply default secondary sorting + if (!appliedUserSort) + { + sortedTeams = sortedTeams + .ThenBy(t => t.Event.Name) + .ThenBy(t => t.Identifier ?? ""); + } + else + { + // Add default sorting as tie-breakers + if (state.SortDefinitions?.First().SortBy != "Event.Name") + { + sortedTeams = sortedTeams.ThenBy(t => t.Event.Name); + } + if (state.SortDefinitions?.First().SortBy != "Identifier") + { + sortedTeams = sortedTeams.ThenBy(t => t.Identifier ?? ""); + } + } + + var totalItems = sortedTeams.Count(); + var pagedData = sortedTeams.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray(); return new GridData { diff --git a/WebApp/Components/Features/Teams/Printout.razor b/WebApp/Components/Features/Teams/Printout.razor index cb19aad..f9a2cd9 100644 --- a/WebApp/Components/Features/Teams/Printout.razor +++ b/WebApp/Components/Features/Teams/Printout.razor @@ -220,8 +220,9 @@ else = await Context.Teams .Include(e => e.Event) .Include(e => e.Students) - .OrderBy(e => e.Event.Name) - .ThenBy(e => e.Identifier) + .OrderByEventFormatFirst() + .ThenBy(e => e.Event.Name) + .ThenBy(e => e.Identifier ?? "") .ToArrayAsync(); _maxTeamSize = _teams.Max(t => t.Students.Count); diff --git a/WebApp/Models/TeamExtensions.cs b/WebApp/Models/TeamExtensions.cs new file mode 100644 index 0000000..aa606e0 --- /dev/null +++ b/WebApp/Models/TeamExtensions.cs @@ -0,0 +1,46 @@ +using Core.Entities; + +namespace WebApp.Models; + +/// +/// Extension methods for sorting teams with group teams (EventFormat.Team) appearing before individual teams (EventFormat.Individual). +/// +public static class TeamExtensions +{ + /// + /// Orders teams with group teams (EventFormat.Team) first, then individual teams (EventFormat.Individual). + /// For use with EF Core IQueryable queries. + /// + /// The queryable collection of teams to sort. + /// An ordered queryable with group teams first, then individual teams. + public static IOrderedQueryable OrderByEventFormatFirst(this IQueryable teams) + { + return teams.OrderByDescending(t => t.Event.EventFormat == EventFormat.Team ? 1 : 0); + } + + /// + /// Orders teams with group teams (EventFormat.Team) first, then individual teams (EventFormat.Individual). + /// For use with in-memory IEnumerable collections. + /// + /// The collection of teams to sort. + /// An ordered enumerable with group teams first, then individual teams. + public static IOrderedEnumerable OrderByEventFormatFirst(this IEnumerable teams) + { + return teams.OrderByDescending(t => t.Event.EventFormat == EventFormat.Team ? 1 : 0); + } + + /// + /// Orders teams with group teams first, then applies default secondary sorting: + /// Event.Name (ascending), then Identifier (ascending with nulls last). + /// + /// The collection of teams to sort. + /// An ordered enumerable with group teams first and default secondary sorting applied. + public static IOrderedEnumerable OrderByEventFormatFirstWithDefaults(this IEnumerable teams) + { + return teams + .OrderByEventFormatFirst() + .ThenBy(t => t.Event.Name) + .ThenBy(t => t.Identifier ?? ""); + } +} + diff --git a/docs/authentication-setup.md b/docs/authentication-setup.md index e7ce86f..b8a5522 100644 --- a/docs/authentication-setup.md +++ b/docs/authentication-setup.md @@ -479,3 +479,4 @@ curl http://localhost:8080 - See `docker-compose.example.yml` for Docker Compose configuration examples - Authentication implementation: `WebApp/Authentication/` +