From 83522ac52c64a09cb5519ee974187976803e3f75 Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Sun, 4 Jan 2026 14:56:28 -0500 Subject: [PATCH] Refactor Teams components for improved sorting and filtering functionality Updated the Teams Index and Printout components to prioritize sorting by EventFormat, enhancing the organization of team data. Introduced a regional filter toggle in the Teams Index to allow users to view only regional teams. Adjusted the ScheduledTeamsList to sort teams by EventFormat first, ensuring consistent ordering across components. Additionally, added necessary using directives for improved code clarity. --- WebApp/Components/Features/Events/Index.razor | 9 +- .../MeetingSchedule/ScheduledTeamsList.razor | 3 +- .../Teams/Components/TeamToggleSelector.razor | 4 +- WebApp/Components/Features/Teams/Index.razor | 101 ++++++++++++++++-- .../Components/Features/Teams/Printout.razor | 5 +- WebApp/Models/TeamExtensions.cs | 46 ++++++++ docs/authentication-setup.md | 1 + 7 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 WebApp/Models/TeamExtensions.cs 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/` +