1c7e704ad3
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
147 lines
5.4 KiB
Plaintext
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
|
|
<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);
|
|
}
|
|
}
|