Files
chapter-organizer/WebApp/Components/Features/Students/EventRanking.razor
T
poprhythm 1c7e704ad3 Add comprehensive validation system with runtime configuration
Implements a flexible validation framework for student rankings, team assignments, and team composition with administrator-configurable rules and thresholds.

Validation System:
- Reflection-based rule discovery eliminates manual registration
- Base classes (RequiredEventTypeRuleBase, EventCountThresholdRuleBase) reduce code duplication by ~250 lines
- 12 validation rules covering event requirements, counts, and team constraints
- Configurable severity levels (Warning/Error) per rule type
- ValidationService with caching for optimal performance
- Rules apply contextually (StudentRanking, StudentAssignment, TeamComposition)

Configuration & Admin UI:
- ValidationSettings admin page for editing thresholds and severity levels
- ChapterSettings admin page for editing chapter information
- Settings stored in Data/appsettings.json for runtime configuration
- JSON config works in both development and production environments
- Auto-creates Data directory and config template on first run

User Experience:
- ValidationWarnings component displays inline warnings/errors
- Integrated in Event Ranking, Registration, and Team Assignment pages
- Color-coded severity indicators (warning yellow, error red)
- Includes "Too Many Regional Events" rule (max 3 recommended)

Technical Improvements:
- Template Method pattern for rule base classes
- Singleton rule instances with lazy initialization
- Configuration loaded via IConfiguration with fallback to defaults
- Safe JSON updates preserve other appsettings sections
2025-12-13 22:15:16 -05:00

147 lines
5.4 KiB
Plaintext

@page "/students/event-ranking"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using Core.Validation
@inject AppDbContext Context
@inject ValidationService ValidationService
@rendermode InteractiveServer
<PageTitle>Student Event Ranks - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3"><MudIcon Icon="@AppIcons.EventRank"></MudIcon> Student Event Ranks</MudText>
@if (_students == null)
{
<p><em>Loading...</em></p>
}
else
{
<MudTable Items="_students" Hover="true" Breakpoint="Breakpoint.Sm" LoadingProgressColor="Color.Info">
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>Grade</MudTh>
<MudTh>TSA Year</MudTh>
<MudTh></MudTh>
<MudTh>Warnings</MudTh>
@for (var i = 1; i <= 10; i++)
{
var ii = i;
<MudTh>@AppIcons.GetOrdinal(ii)</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd>@context.FirstName</MudTd>
<MudTh>@context.Grade</MudTh>
<MudTh>@context.TsaYear</MudTh>
<MudTd><MudButton StartIcon="@Icons.Material.Filled.TableChart" Href="@($"students/event-ranking-edit/{context.Id}")">Edit</MudButton></MudTd>
<MudTd>
<ValidationWarnings Warnings="@GetStudentWarnings(context)" />
</MudTd>
@for (var i = 1; i <= 10; i++)
{
var ii = i;
var st = context.EventRankings.FirstOrDefault(e => e.Rank == i);
<MudTd Class="@($"event-rank-{ii})")">
@if (st != null)
{
<span>@st.EventDefinition.ShortName&nbsp;
<EventAttributes EventDefinition="@st.EventDefinition"></EventAttributes>
</span>
}
</MudTd>
}
</RowTemplate>
</MudTable>
<MudTable Items="_eventStudentRankings" Hover="true" Breakpoint="Breakpoint.Sm" LoadingProgressColor="Color.Info">
<HeaderContent>
<MudTh>Event</MudTh>
<MudTh>Level of Effort</MudTh>
<MudTh>Individual</MudTh>
<MudTh>Regional</MudTh>
<MudTh>On-site Activity</MudTh>
<MudTh>Team Size</MudTh>
@for (var i = 0; i < _maxEventStudentRankings; i++)
{
var i1 = i + 1;
<MudTh>@i1</MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd>@context.Event.Name</MudTd>
<MudTd>@context.Event.LevelOfEffort</MudTd>
<MudTd>@if (context.Event.EventFormat == EventFormat.Individual) { <span>ⓘ</span> }</MudTd >
<MudTd>@if (context.Event.RegionalEvent) { <span>ⓡ</span> }</MudTd >
<MudTd>@if (context.Event.OnSiteActivity) { <span>ⓐ</span> }</MudTd >
<MudTd>[@context.Event.MinTeamSize-@context.Event.MaxTeamSize]</MudTd >
@for (var j = 0; j < _maxEventStudentRankings; j++)
{
var student = j < context.StudentRanking.Length ? context.StudentRanking[j] : null;
var eventClass = student != null ? $"event-rank-{student.Item2}" : "";
<MudTd Class="@eventClass">
@if (student != null)
{
@student.Item1.FirstName
}
</MudTd>
}
</RowTemplate>
</MudTable>
}
@code {
private Student[]? _students;
private class EventStudentRankings {
public EventDefinition Event {get; set; }
public Tuple<Student,int> [] StudentRanking { get; set; }
}
private EventStudentRankings[] _eventStudentRankings;
private int _maxEventStudentRankings;
protected override async Task OnInitializedAsync()
{
_students =
await Context.Students
.Include(e => e.EventRankings)
.ThenInclude(e => e.EventDefinition)
.OrderBy(e => e.FirstName).ToArrayAsync();
_eventStudentRankings =
_students.SelectMany(s =>
s.EventRankings,
(student, ranking) => new { e = ranking.EventDefinition, a = Tuple.Create(student, ranking.Rank) }
)
.GroupBy(e => e.e)
.Select(e =>
new EventStudentRankings
{
Event = e.Key,
StudentRanking = e.Select(er => er.a).OrderBy(ser => ser.Item2).ThenByDescending(ser => ser.Item1.Grade + ser.Item1.TsaYear).ToArray()
})
.OrderBy(e => e.Event.Name)
.ToArray();
var events = await Context.Events.ToArrayAsync();
var remainingEvents =
events
.Where(e => _eventStudentRankings.All(est => est.Event.Id != e.Id))
.Select(e => new EventStudentRankings { Event = e, StudentRanking = Array.Empty<Tuple<Student, int>>() })
.OrderBy(e => e.Event.Name)
.ToArray();
_eventStudentRankings = _eventStudentRankings.Concat(remainingEvents).ToArray();
_maxEventStudentRankings = _eventStudentRankings.Max(esr => esr.StudentRanking.Length);
}
private List<ValidationWarning> GetStudentWarnings(Student student)
{
return ValidationService.ValidateStudentRankings(student, ValidationContext.StudentRanking);
}
}