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/`
+