Files
chapter-organizer/WebApp/Components/Features/Students/Registration.razor
T
2025-12-12 12:48:44 -05:00

248 lines
10 KiB
Plaintext

@page "/students/teams"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context
@inject WebApp.LocalStorageService LocalStorage
<PageTitle>Registration - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Registration</MudText>
<MudButtonGroup>
<MudButton StartIcon="@Icons.Material.Filled.FilterAlt"
Variant="@(_showRegionalOnly ? Variant.Filled : Variant.Outlined)"
Color="Color.Primary"
OnClick="ToggleRegionalFilter">
@(_showRegionalOnly ? "Showing Regional Only" : "Show Regional Only")
</MudButton>
</MudButtonGroup>
<MudText Typo="Typo.body2" Style="margin-top: 8px; margin-bottom: 4px;">
<MudIcon Icon="@AppIcons.Captain" Size="Size.Small" /> = Team Captain
</MudText>
<MudText Typo="Typo.body2" Style="margin-bottom: 8px;">
<strong>Show Columns:</strong>
<MudCheckBox Value="_showGrade" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showGrade), val, v => _showGrade = v)" Label="Grade" Dense="true" Size="Size.Small" />
<MudCheckBox Value="_showRegionalId" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showRegionalId), val, v => _showRegionalId = v)" Label="Regional ID" Dense="true" Size="Size.Small" />
<MudCheckBox Value="_showStateId" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showStateId), val, v => _showStateId = v)" Label="State ID" Dense="true" Size="Size.Small" />
<MudCheckBox Value="_showNationalId" ValueChanged="async (bool val) => await OnColumnToggle(nameof(_showNationalId), val, v => _showNationalId = v)" Label="National ID" Dense="true" Size="Size.Small" />
</MudText>
<MudDataGrid T="StudentTeamInfo" ServerData="ServerReload" @ref="_dataGrid" @key="_gridKey" Filterable="true" RowsPerPage="35">
<Columns>
<PropertyColumn Property="@(e => e.Student.LastName)" Title="Student" Sortable="true">
<CellTemplate>
@context.Item.Student.LastNameFirstName
<MudTooltip Text="Edit">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Href="@($"/students/edit?id={context.Item.Student.Id}&returnUrl=/students/teams")"
Style="margin-left: 4px;" />
</MudTooltip>
</CellTemplate>
</PropertyColumn>
@if (_showGrade)
{
<PropertyColumn Property="@(e => e.Student.Grade)" Title="Grade" Sortable="true" />
}
@if (_showRegionalId)
{
<PropertyColumn Property="@(e => e.Student.RegionalId)" Title="Regional ID" Sortable="true" />
}
@if (_showStateId)
{
<PropertyColumn Property="@(e => e.Student.StateId)" Title="State ID" Sortable="true" />
}
@if (_showNationalId)
{
<PropertyColumn Property="@(e => e.Student.NationalId)" Title="National ID" Sortable="true" />
}
<TemplateColumn Title="Events">
<CellTemplate>
@if (context.Item.Teams != null)
{
var teamsToDisplay = _showRegionalOnly
? context.Item.Teams.Where(t => t?.Event != null && t.Event.RegionalEvent).OrderBy(t => t.Event.Name)
: context.Item.Teams.Where(t => t?.Event != null).OrderBy(t => t.Event.Name);
foreach (var team in teamsToDisplay)
{
var isCaptain = team.Captain != null && team.Captain.Equals(context.Item.Student);
var teamMembers = string.Join(", ", team.Students.Select(s => s.FirstName));
<MudTooltip Text="@teamMembers">
<MudChip Size="Size.Small"
Color="Color.Default"
Href="@($"/teams/edit?id={team.Id}")"
Style="cursor: pointer;">
@team
@if (isCaptain)
{
<MudIcon Icon="@AppIcons.Captain" Size="Size.Small" Style="margin-left: 4px;" />
}
</MudChip>
</MudTooltip>
}
}
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="StudentTeamInfo" PageSizeOptions="new int[] { 10, 25, 35, 50, 100 }"></MudDataGridPager>
</PagerContent>
</MudDataGrid>
@code {
MudDataGrid<StudentTeamInfo> _dataGrid = null!;
private bool _showRegionalOnly;
private bool _showGrade;
private bool _showRegionalId;
private bool _showStateId;
private bool _showNationalId;
private bool _preferencesLoaded = false;
// TODO: Remove this workaround once MudBlazor fixes dynamic column ordering
// https://dev.to/the_real_slim_janey/get-in-line-customizing-column-order-in-mudblazor-3ail
// To remove:
// 1. Delete this _gridKey field
// 2. Remove "_gridKey++;" from all checkbox ValueChanged handlers (lines 26-29)
// 3. Remove "@key="_gridKey"" attribute from MudDataGrid (line 32)
private int _gridKey = 0;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_preferencesLoaded)
{
await LoadColumnPreferences();
_preferencesLoaded = true;
_gridKey++; // Force grid recreation with loaded preferences
StateHasChanged();
}
}
protected override void OnInitialized()
{
// Reset flag when component is initialized/re-initialized
_preferencesLoaded = false;
}
private async Task LoadColumnPreferences()
{
_showGrade = await LocalStorage.GetBoolAsync("Registration_ShowGrade", false);
_showRegionalId = await LocalStorage.GetBoolAsync("Registration_ShowRegionalId", false);
_showStateId = await LocalStorage.GetBoolAsync("Registration_ShowStateId", false);
_showNationalId = await LocalStorage.GetBoolAsync("Registration_ShowNationalId", false);
}
private async Task OnColumnToggle(string columnName, bool value, Action<bool> setter)
{
setter(value);
_gridKey++;
await LocalStorage.SetBoolAsync($"Registration_{columnName}", value);
StateHasChanged();
}
private async Task ToggleRegionalFilter()
{
_showRegionalOnly = !_showRegionalOnly;
StateHasChanged();
await Task.Delay(10);
await _dataGrid.ReloadServerData();
}
private async Task<GridData<StudentTeamInfo>> ServerReload(GridState<StudentTeamInfo> state)
{
// Load all students with their teams
var students = await Context.Students
.Include(s => s.Teams)
.ThenInclude(t => t.Event)
.Include(s => s.Teams)
.ThenInclude(t => t.Captain)
.ToListAsync();
// Filter to only students with teams
var studentTeams = students
.Where(s => s.Teams.Any(t => t?.Event != null && (!_showRegionalOnly || t.Event.RegionalEvent)))
.Select(s => new StudentTeamInfo
{
Student = s,
Teams = s.Teams?.Where(t => t?.Event != null).ToList() ?? []
})
.ToList();
// Apply sorting
var sortedData = ApplySorting(studentTeams, state.SortDefinitions);
var totalItems = sortedData.Count();
var pagedData = sortedData.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray();
return new GridData<StudentTeamInfo>
{
TotalItems = totalItems,
Items = pagedData
};
}
/// <summary>
/// Dictionary mapping column property names to their corresponding sort expressions.
/// Used to provide type-safe sorting for data grid columns.
/// </summary>
/// <remarks>
/// Each entry maps a property path (e.g., "Student.LastName") to a function that
/// extracts the comparable value from a StudentTeamInfo instance.
/// Nullable strings are coalesced to empty strings to ensure consistent sorting behavior.
/// </remarks>
private static readonly Dictionary<string, Func<StudentTeamInfo, IComparable>> SortExpressions = new()
{
{ "Student.LastName", s => s.Student.LastName },
{ "Student.Grade", s => s.Student.Grade },
{ "Student.RegionalId", s => s.Student.RegionalId ?? string.Empty },
{ "Student.StateId", s => s.Student.StateId ?? string.Empty },
{ "Student.NationalId", s => s.Student.NationalId ?? string.Empty }
};
/// <summary>
/// Applies sorting to the student team data based on the provided sort definitions.
/// </summary>
/// <param name="data">The list of StudentTeamInfo records to sort.</param>
/// <param name="sortDefinitions">Collection of sort definitions from the MudDataGrid state.</param>
/// <returns>
/// An IEnumerable of StudentTeamInfo sorted according to the first sort definition,
/// or sorted by LastName if no valid sort definition is provided.
/// </returns>
/// <remarks>
/// If no sort definitions are provided, or if the requested property is not found in SortExpressions,
/// the data will default to sorting by Student.LastName in ascending order.
/// Only the first sort definition is applied (multi-column sorting is not supported).
/// </remarks>
private IEnumerable<StudentTeamInfo> ApplySorting(
List<StudentTeamInfo> data,
ICollection<SortDefinition<StudentTeamInfo>> sortDefinitions)
{
if (!sortDefinitions.Any())
{
return data.OrderBy(s => s.Student.LastName);
}
var sortDef = sortDefinitions.First();
var propertyName = sortDef.SortBy;
if (!SortExpressions.TryGetValue(propertyName, out var sortExpression))
{
return data.OrderBy(s => s.Student.LastName);
}
return sortDef.Descending
? data.OrderByDescending(sortExpression)
: data.OrderBy(sortExpression);
}
public class StudentTeamInfo
{
public required Student Student { get; init; }
public List<Team> Teams { get; init; } = [];
}
}