317 lines
13 KiB
Plaintext
317 lines
13 KiB
Plaintext
@page "/students/teams"
|
|
@attribute [Authorize]
|
|
@using Microsoft.EntityFrameworkCore
|
|
@using WebApp.Models
|
|
@using WebApp.Components.Shared.Components
|
|
@using Core.Validation
|
|
@using Core.Utility
|
|
@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>
|
|
<PageNoteButton PageIdentifier="Registration" />
|
|
</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 = TeamStudentNameFormatter.FormatStudentList(
|
|
team,
|
|
new TeamStudentNameFormatter.FormatOptions
|
|
{
|
|
Ordering = TeamStudentNameFormatter.OrderingStyle.None
|
|
});
|
|
|
|
<MudTooltip Text="@teamMembers">
|
|
<MudChip Size="Size.Small"
|
|
Color="Color.Default"
|
|
Variant="@AppIcons.TeamChipVariant()"
|
|
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; } = [];
|
|
}
|
|
}
|