Files
chapter-organizer/WebApp/Components/Features/Students/Registration.razor
T
poprhythm 6acbc4e852 Enhance authentication flow by adding return URL support
This commit updates the authentication process to include a return URL parameter, allowing users to be redirected back to their original page after logging in. Changes were made to the AuthController, Login component, and Routes component to handle the return URL appropriately. Additionally, improvements were made to the TeamScheduler and TeamSchedulerSolution classes for better team and student management. These enhancements improve user experience and navigation within the application.
2026-01-11 13:13:24 -05:00

309 lines
13 KiB
Plaintext

@page "/students/teams"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using WebApp.Components.Shared.Components
@using Core.Validation
@inject AppDbContext Context
@inject WebApp.LocalStorageService LocalStorage
@inject ValidationService ValidationService
<PageHeader Title="Registration" Icon="@AppIcons.Registration">
<ActionButtons>
<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>
</ActionButtons>
</PageHeader>
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
<MudText Typo="Typo.body2" Class="mb-2">
<MudIcon Icon="@AppIcons.Captain" Size="Size.Small" /> = Team Captain
</MudText>
<MudText Typo="Typo.body2" Class="mb-3">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1">
<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"/>
</MudStack>
</MudText>
<MudDataGrid T="StudentTeamInfo" ServerData="ServerReload" @ref="_dataGrid" @key="_gridKey" Filterable="true" RowsPerPage="35" Dense="true" Striped="true" Hover="true" Loading="@_isLoading" LoadingProgressColor="Color.Primary">
<Columns>
<PropertyColumn Property="@(e => e.Student.LastName)" Title="Student" Sortable="true">
<CellTemplate>
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1">
@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>
</MudStack>
</CellTemplate>
</PropertyColumn>
<TemplateColumn Title="Warnings" Sortable="false">
<CellTemplate>
<ValidationWarnings Warnings="@GetRegistrationWarnings(context.Item.Student)" />
</CellTemplate>
</TemplateColumn>
@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>
@{
var teamsToDisplay = _showRegionalOnly
? context.Item.Teams.Where(t => t?.Event is { RegionalEvent: true }).OrderBy(t => t.Event.Name)
: context.Item.Teams.Where(t => t?.Event != null).OrderBy(t => t.Event.Name);
}
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
@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}&returnUrl=/students/teams")"
Style="cursor: pointer;">
@team
@if (isCaptain && team.Event.EventFormat != EventFormat.Individual)
{
<MudIcon Icon="@AppIcons.Captain" Size="Size.Small" Style="margin-left: 4px;" />
}
</MudChip>
</MudTooltip>
}
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="StudentTeamInfo" PageSizeOptions="new int[] { 10, 25, 35, 50, 100 }"></MudDataGridPager>
</PagerContent>
</MudDataGrid>
</MudPaper>
@code {
MudDataGrid<StudentTeamInfo> _dataGrid = null!;
private bool _isLoading = true;
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)
{
_isLoading = true;
try
{
// Load teams with their students separately to avoid circular reference
var teamsWithStudents = await Context.Teams
.AsNoTracking()
.Include(t => t.Students)
.Include(t => t.Event)
.Include(t => t.Captain)
.ToListAsync();
// Create a dictionary for quick lookup of team students
var teamStudentsDict = teamsWithStudents.ToDictionary(t => t.Id, t => t.Students.ToList());
// Load all students with their teams (without loading team students to avoid circular reference)
var students = await Context.Students
.AsNoTracking()
.Include(s => s.Teams)
.ThenInclude(t => t.Event)
.Include(s => s.Teams)
.ThenInclude(t => t.Captain)
.ToListAsync();
// Populate Students for each team from the separately loaded teams
foreach (var student in students)
{
foreach (var team in student.Teams)
{
if (teamStudentsDict.TryGetValue(team.Id, out var teamStudents))
{
team.Students = teamStudents;
}
}
}
// 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
};
}
finally
{
_isLoading = false;
}
}
/// <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);
}
private List<ValidationWarning> GetRegistrationWarnings(Student student)
{
// Create StudentEventStatistics from the student's teams
var stats = new StudentEventStatistics
{
Student = student,
Events = student.Teams?.Where(t => t?.Event != null)
.Select(t => t.Event)
.ToList() ?? []
};
return ValidationService.ValidateStudentStatistics(stats, ValidationContext.StudentRegistration);
}
public class StudentTeamInfo
{
public required Student Student { get; init; }
public List<Team> Teams { get; init; } = [];
}
}