Feature-based folder structure

1. Created feature-based folder structure - Components now organized by domain feature
  2. Moved all components - 20+ files moved to new locations
  3. Updated _Imports.razor - Added all new namespace paths for global component access
  4. Updated CustomThemes.cs namespace - Changed from WebApp.Components.Layout to WebApp.Components.Shared.Layout
  5. Removed old using directives - Cleaned up Login.razor and Routes.razor
  6. Removed empty directories - Cleaned up old folder structure
This commit is contained in:
2025-12-03 22:04:23 -05:00
parent bd04483bed
commit 2d5d075879
38 changed files with 13 additions and 4 deletions
@@ -0,0 +1,464 @@
@page "/teams/assignment"
@attribute [Authorize]
@using Core.Calculation
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using EventAssignment = Core.Calculation.EventAssignment
@inject AppDbContext Context
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<PageTitle>Event Assignment - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Assignment</MudText>
<MudGrid>
<MudItem><MudText>Optimized team assignments based on the student event rankings</MudText></MudItem>
<MudItem><MudButton StartIcon="@Icons.Material.Filled.Edit" Href="students/event-ranking">Edit Student Event Rankings</MudButton></MudItem>
</MudGrid>
<MudPaper Class="pa-4 mt-5">
<MudGrid>
<MudItem Style="width:160px;">
<MudNumericField @bind-Value="_parameters.TeamSizeLimit"
Label="Team Size Limit" Min="3" Max="8"></MudNumericField>
</MudItem>
<MudItem>
<MudTooltip Text="Require at least one On-Site Event">
<MudSwitch @bind-Value="_parameters.RequireOnSite" Color="Color.Info"
Label="On-Site" />
</MudTooltip>
</MudItem>
<MudItem>
<MudTooltip Text="Require at least one Regional Event">
<MudSwitch @bind-Value="_parameters.RequireRegional" Color="Color.Info"
Label="Regional" />
</MudTooltip>
</MudItem>
<MudItem>
<MudStack Style="width:100px;">
<MudTooltip Text="Student Event Count Assignment Range">
<MudInputLabel>Event Count</MudInputLabel>
</MudTooltip>
<MudNumericField @bind-Value="_parameters.EventsLowerBound"
Label="At Least" Min="2" Max="4"></MudNumericField>
<MudNumericField @bind-Value="_parameters.EventsUpperBound"
Label="Up to" Min="3" Max="5"></MudNumericField>
</MudStack>
</MudItem>
<MudItem>
<MudStack Style="width:100px;">
<MudTooltip Text="Student Level of Effort Range">
<MudInputLabel>LOE</MudInputLabel>
</MudTooltip>
<MudNumericField @bind-Value="_parameters.EffortLowerBound"
Label="At Least" Min="4" Max="7"></MudNumericField>
<MudNumericField @bind-Value="_parameters.EffortUpperBound"
Label="Up to" Min="7" Max="12"></MudNumericField>
</MudStack>
</MudItem>
<MudItem>
<MudInputLabel>Assignment Requirements</MudInputLabel>
<MudTable T="AssignmentRequirement" ServerData="ReloadAssignmentRequirements" @ref="_assignmentRequirementData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveRequireEvent(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">
@item.Student.FirstName
@item.EventDefinition.ShortName
@if (item.Requirement == Requirement.Include)
{
<MudIcon Class="ml-3" Icon="@Icons.Material.Filled.ThumbUp" Size="Size.Small"></MudIcon>
}
@if (item.Requirement == Requirement.Exclude)
{
<MudIcon Class="ml-3" Icon="@Icons.Material.Filled.ThumbDownAlt" Size="Size.Small"></MudIcon>
}
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem>
<MudInputLabel>Two Team Events</MudInputLabel>
<MudTable T="EventDefinition" ServerData="ReloadEventTwoTeam" @ref="_eventTwoTeamData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveTwoTeam(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.ShortName</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem>
<MudInputLabel>Omitted Events</MudInputLabel>
<MudTable T="EventDefinition" ServerData="ReloadOmittedEvents" @ref="_eventOmittedData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveOmitted(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.ShortName</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
</MudGrid>
<MudButton Class="ma-3" OnClick="Solve" Variant="Variant.Filled" Color="Color.Primary" Disabled="@_isSolving">Solve</MudButton>
</MudPaper>
<MudGrid>
<MudItem xs="12" lg="8">
<MudText Typo="Typo.h4">Students</MudText>
<MudTable T="StudentEventStatistics" ServerData="ReloadStatistics" @ref="_statisticData" >
<ColGroup>
<col style="width: 150px;" />
<col style="width: 50px;" />
<col style="width: 50px;" />
<col style="width: 60px;" />
</ColGroup>
<HeaderContent>
<MudTh>Student</MudTh>
<MudTh><MudTooltip Text="How many events they're assigned'">Event Count</MudTooltip></MudTh>
<MudTh><MudTooltip Text="Level of Effort Total">LOE Sum</MudTooltip></MudTh>
<MudTh><MudTooltip Text="Assignment Warnings">Warnings</MudTooltip></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><b>@context.Student.FirstName</b></MudTd>
<MudTd>@context.EventCount</MudTd>
<MudTd>@context.TotalLevelOfEffort</MudTd>
<MudTd>@if (!context.HasOnSiteActivity)
{
<MudTooltip Text="No On-Site Activity">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.OnSiteActivity</MudText>
</MudTooltip>
}
@if (!context.HasRegionalEvent)
{
<MudTooltip Text="No Regional Event">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.RegionalEvent</MudText>
</MudTooltip>
}
</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTr><td colspan="4">
@{
var allStudentEvents =
context.Student.EventRankings
.OrderBy(e => e.Rank)
.Select(e => e.EventDefinition)
.Concat(context.Events)
.Distinct();
}
@foreach (var e in
allStudentEvents
.OrderBy(e =>
context.Student.EventRankings
.Find(ser => ser.EventDefinition == e)?.Rank ?? 10))
{
var eventRank = context.Student.EventRankings.Find(er => er.EventDefinition == e)?.Rank;
var isAssigned = context.Events.Contains(e);
var color = AppIcons.RankedEventColor(eventRank ?? 0);
var style = string.Empty;
if (isAssigned)
{
style += "border-color:black; border-width:thin;";
if (eventRank.HasValue)
{
style += $"background:{color};";
if (eventRank == 1)
style += $"color:black";
}
else
style += $"background:{Colors.Gray.Lighten3};";
}
else
{
if (eventRank.HasValue)
style += $"border-color:{color}; border-width:medium; color:{Colors.Gray.Lighten1};";
}
<MudPaper Class="d-inline-flex align-center pa-2 mx-3 my-1 border-solid" Style="@(style)">
@e.ShortName&nbsp;
@AppIcons.EventAttributes(e)
@{
var isIncluded = _assignmentRequirements
.Find(ar =>
ar.EventDefinition == e
&& ar.Student == context.Student
&& ar.Requirement == Requirement.Include) == null;
var isExcluded = _assignmentRequirements
.Find(ar =>
ar.EventDefinition == e
&& ar.Student == context.Student
&& ar.Requirement == Requirement.Exclude) == null;
}
@if (isIncluded)
{
<MudTooltip Text="@($"Add requirement for {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Outlined.ThumbUpAlt" Class="ml-3" Size="Size.Small" Color="Color.Default"
OnClick="() => RequireEvent(e, context.Student, Requirement.Include)"></MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="@($"Remove requirement for {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Filled.ThumbUpAlt" Class="ml-3" Size="Size.Small" Color="Color.Dark"
OnClick="() => RemoveRequireEvent(e, context.Student, Requirement.Include)"></MudIconButton>
</MudTooltip>
}
@if (isExcluded)
{
<MudTooltip Text="@($"Add restriction against {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Outlined.ThumbDownAlt" Size="Size.Small" Color="Color.Default"
OnClick="() => RequireEvent(e, context.Student, Requirement.Exclude)"></MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="@($"Remove restriction against {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Filled.ThumbDownAlt" Size="Size.Small" Color="Color.Dark"
OnClick="() => RemoveRequireEvent(e, context.Student, Requirement.Exclude)"></MudIconButton>
</MudTooltip>
}
</MudPaper>
}
<MudDivider Style="border-width:3px" />
</td></MudTr>
</ChildRowContent>
</MudTable>
</MudItem>
<MudItem xs="12" lg="4">
<MudText Typo="Typo.h4">Teams</MudText>
<MudTable T="Team" ServerData="SolveAssignments" @ref="_teamData">
<ColGroup>
<col style="width: 200px;" />
<col style="width: 40px; white-space:nowrap" />
</ColGroup>
<HeaderContent>
<MudTh>Team</MudTh>
<MudTh><MudTooltip Text="Number of Student Rankings, Number of TimeSlots [Eligibility Lower Bound-Upper Bound]"><MudText Style="white-space:nowrap;">R, # [LB-UB]</MudText> </MudTooltip></MudTh>
</HeaderContent>
<RowTemplate>
@{
var thresholds = _eventAssignmentThresholds.First(e => e.Event == context.Event);
}
<MudTd>
<b>@context.Event.Name</b>&nbsp;
<EventAttributes EventDefinition="context.Event"></EventAttributes>
</MudTd>
<MudTd Style="white-space:nowrap">
@thresholds.StudentRankingCount, [@thresholds.LowerBound-@thresholds.UpperBound] &times; @thresholds.TeamCount
@if (_eventTwoTeams.Contains(context.Event))
{
<MudIconButton Class="ml-3" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.PlusOne" Variant="Variant.Filled"
OnClick="() => RemoveTwoTeam(context.Event)"></MudIconButton>
}
else if (thresholds.TeamCount != 2 && context.Event.EventFormat == EventFormat.Team)
{
<MudIconButton Class="ml-3" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.PlusOne"
OnClick="() => AddTwoTeam(context.Event)"></MudIconButton>
}
<MudIconButton Icon="@Icons.Material.Outlined.Remove" Size="Size.Small" Color="Color.Default"
OnClick="() => AddOmitted(context.Event)"></MudIconButton>
</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTr>
<td colspan="2">
<TeamStudents Team="@context"></TeamStudents>
<MudDivider Style="border-width:3px"/>
</td>
</MudTr>
</ChildRowContent>
<NoRecordsContent>
<MudText Color="Color.Warning">Solution status: @_solutionStatus</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
</MudTable>
</MudItem>
</MudGrid>
<MudTooltip Text="This will overwrite existing teams">
<MudButton Class="ma-3" StartIcon="@Icons.Material.Filled.Warning" Variant="Variant.Filled" Size="Size.Large" OnClick="SaveTeams" Color="Color.Warning">Save</MudButton>
</MudTooltip>
@code {
public bool TestSwitch { get; set; } = false;
private readonly AssignmentParameters _parameters = new() { RequireOnSite = false, RequireRegional = false };
private List<EventDefinition>? _events;
private List<Student>? _students;
private List<EventAssignmentThresholds> _eventAssignmentThresholds = [];
MudTable<Team> _teamData;
private Team[] _teams = [];
MudTable<StudentEventStatistics> _statisticData;
private List<StudentEventStatistics> _statistics = [];
MudTable<AssignmentRequirement> _assignmentRequirementData;
private List<AssignmentRequirement> _assignmentRequirements = [];
MudTable<EventDefinition> _eventTwoTeamData;
private List<EventDefinition> _eventTwoTeams = [];
MudTable<EventDefinition> _eventOmittedData;
private List<EventDefinition> _eventOmitted = [];
private string _solutionStatus = string.Empty;
private bool _isSolving = false;
protected override async Task OnInitializedAsync()
{
_events =
await Context.Events
.OrderBy(e => e.Name)
.ToListAsync();
_students =
await Context.Students
.Where(e => e.FirstName != "test")
.Include(e => e.EventRankings)
.ThenInclude(e => e.EventDefinition)
.Where(e => e.EventRankings.Any())
.OrderBy(e => e.FirstName).ToListAsync();
}
private async Task AddTeam()
{
//Context.TimeSlots.Add(Team);
await Context.SaveChangesAsync();
//NavigationManager.NavigateTo("/teams");
}
private void Solve()
{
_teamData.ReloadServerData();
}
private async Task<TableData<Team>> SolveAssignments(TableState arg1, CancellationToken arg2)
{
_isSolving = true;
var eventAssignment = new EventAssignment(_events, _students, _parameters);
foreach (var requirement in _assignmentRequirements)
{
eventAssignment.AddAssignmentRequirement(requirement);
}
eventAssignment.RemoveEvents(_eventOmitted);
eventAssignment.AllowTwoTeams(_eventTwoTeams);
var solution = await eventAssignment.Solve();
_solutionStatus = solution.Status;
_statistics =
StudentEventStatistics.Generate(solution.Teams)
.OrderByDescending(s => s.Student.Grade + s.Student.TsaYear)
.ThenBy(s => s.Student.FirstName).ToList();
_eventAssignmentThresholds = solution.AssignmentThresholds;
await _statisticData.ReloadServerData();
_isSolving = false;
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
_teams = solution.Teams;
return new TableData<Team> { Items = _teams };
}
private async Task<TableData<StudentEventStatistics>> ReloadStatistics(TableState arg1, CancellationToken arg2)
{
return new TableData<StudentEventStatistics> {Items = _statistics};
}
private async Task<TableData<AssignmentRequirement>> ReloadAssignmentRequirements(TableState arg1, CancellationToken arg2)
{
return new TableData<AssignmentRequirement> { Items = _assignmentRequirements };
}
private async Task<TableData<EventDefinition>> ReloadEventTwoTeam(TableState arg1, CancellationToken arg2)
{
return new TableData<EventDefinition> { Items = _eventTwoTeams };
}
private async Task<TableData<EventDefinition>> ReloadOmittedEvents(TableState arg1, CancellationToken arg2)
{
return new TableData<EventDefinition> { Items = _eventOmitted };
}
private void RequireEvent(EventDefinition evt, Student student, Requirement requirement)
{
_assignmentRequirements.Add(new AssignmentRequirement(evt, student, requirement));
_assignmentRequirementData.ReloadServerData();
}
private void RemoveRequireEvent(EventDefinition evt, Student student, Requirement requirement)
{
var assignmentRequirement =
_assignmentRequirements
.Find(ar => ar.EventDefinition == evt && ar.Student == student && ar.Requirement == requirement);
if (assignmentRequirement != null) RemoveRequireEvent(assignmentRequirement);
}
private void RemoveRequireEvent(AssignmentRequirement assignmentRequirement)
{
_assignmentRequirements.Remove(assignmentRequirement);
_assignmentRequirementData.ReloadServerData();
}
private void AddTwoTeam(EventDefinition evt)
{
_eventTwoTeams.Add(evt);
_eventTwoTeamData.ReloadServerData();
}
private void RemoveTwoTeam(EventDefinition evt)
{
_eventTwoTeams.Remove(evt);
_eventTwoTeamData.ReloadServerData();
}
private void AddOmitted(EventDefinition evt)
{
_eventOmitted.Add(evt);
_eventOmittedData.ReloadServerData();
}
private void RemoveOmitted(EventDefinition evt)
{
_eventOmitted.Remove(evt);
_eventOmittedData.ReloadServerData();
}
public async Task SaveTeams()
{
var result = await DialogService
.ShowMessageBox("Save Teams",
(MarkupString)$"Are you sure want to save these teams? Current teams will be erased. This cannot be undone.",
yesText: "Yes",
noText: "Cancel");
if (result == true)
{
await Context.Teams.ExecuteDeleteAsync();
await Context.Teams.AddRangeAsync(_teams);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/teams");
}
}
}
@@ -0,0 +1,38 @@
@if (Title != null)
{
<MudText Typo="@TitleTypo">@Title</MudText>
}
<MudToggleGroup T="Student"
SelectionMode="SelectionMode.ToggleSelection"
Value="@SelectedCaptain"
ValueChanged="@OnSelectedCaptainChanged"
CheckMark>
@foreach (var student in Students.OrderBy(e => e.FirstName))
{
<MudToggleItem Value="@student" Text="@student.Name" />
}
</MudToggleGroup>
@code {
[Parameter]
public IEnumerable<Student> Students { get; set; } = [];
[Parameter]
public Student? SelectedCaptain { get; set; }
[Parameter]
public EventCallback<Student?> SelectedCaptainChanged { get; set; }
[Parameter]
public string? Title { get; set; } = "Captain";
[Parameter]
public Typo TitleTypo { get; set; } = Typo.body1;
private async Task OnSelectedCaptainChanged(Student? value)
{
SelectedCaptain = value;
await SelectedCaptainChanged.InvokeAsync(value);
}
}
@@ -0,0 +1,18 @@
<MudSelect
T="Team"
MultiSelection="true"
@bind-SelectedValues="@SelectedTeams"
ToStringFunc="e => e.ToString()"
Label="@Label">
@foreach (var evt in Teams.OrderBy(e => e.ToString()))
{
<MudSelectItem T="Team" Value="@evt">@evt.ToString()</MudSelectItem>
}
</MudSelect>
@code {
[Parameter] public Team[]? Teams { get; set; }
[Parameter] public required IEnumerable<Team> SelectedTeams { get; set; }
[Parameter] public string Label { get; set; } = "Teams";
}
@@ -0,0 +1,27 @@
@using WebApp.Models
@foreach (var student in
Team.Students
.OrderBy(e =>
e.EventRankings
.Find(er => er.EventDefinition == Team.Event)?.Rank ?? 10)
.ThenBy(s => s.Grade + s.TsaYear))
{
var eventRank =
student.EventRankings
.Find(e => e.EventDefinition == Team.Event)?.Rank;
var color = AppIcons.RankedEventColor(eventRank ?? 0);
var captain = Team.Captain != null && Team.Captain.Equals(student);
<MudPaper Class="d-inline-flex pa-2 mx-3 my-1" Style="@($"background:{color};")">
@student.FirstName
@if (captain)
{
<span> (Cpt)</span>
}
</MudPaper>
}
@code {
[Parameter]
public Team Team { get; set; }
}
@@ -0,0 +1,49 @@
@if (Title != null)
{
<MudText Typo="Typo.h4">@Title</MudText>
}
<MudToggleGroup T="Team"
SelectionMode="SelectionMode.MultiSelection"
Values="@SelectedTeams"
ValuesChanged="@OnSelectedTeamsChanged"
Vertical="true"
CheckMark>
@foreach (var team in Teams.OrderBy(e => e.Event.Name))
{
<MudToggleItem Value="@team" Style="font-size: .75rem;">
<MudTooltip Text="@team.StudentsFirstNames">
<div class="d-flex align-center justify-space-between flex-wrap">
<MudText Class="ellipsis">@team.ToString()</MudText>
@if (ShowEventAttributes)
{
<EventAttributes EventDefinition="@team.Event"></EventAttributes>
}
</div>
</MudTooltip>
</MudToggleItem>
}
</MudToggleGroup>
@code {
[Parameter]
public IEnumerable<Team> Teams { get; set; } = [];
[Parameter]
public IEnumerable<Team> SelectedTeams { get; set; } = [];
[Parameter]
public EventCallback<IEnumerable<Team>> SelectedTeamsChanged { get; set; }
[Parameter]
public string? Title { get; set; }
[Parameter]
public bool ShowEventAttributes { get; set; } = true;
private async Task OnSelectedTeamsChanged(IEnumerable<Team> value)
{
SelectedTeams = value;
await SelectedTeamsChanged.InvokeAsync(value);
}
}
@@ -0,0 +1,153 @@
@page "/teams/create"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Create Team - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Create</MudText>
<MudText Typo="Typo.h4">Team</MudText>
<MudDivider />
<EditForm method="post" Model="Team" OnValidSubmit="AddTeam" FormName="create" Enhance>
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<MudSelect T="EventDefinition" Value="@Team.Event" ValueChanged="OnEventChanged" Label="Event">
@foreach (var evt in _events)
{
<MudSelectItem T="EventDefinition" Value="@(evt)"></MudSelectItem>
}
</MudSelect>
@if (_existingTeams?.Count == 1)
{
<MudAlert Severity="Severity.Info" Class="my-2">
A team for @Team.Event.Name already exists. This will be team 2, and the existing team will become team 1.
</MudAlert>
}
else if (_existingTeams?.Count >= 2)
{
<MudAlert Severity="Severity.Error" Class="my-2">
Two teams for @Team.Event.Name already exist. Cannot create a third team.
</MudAlert>
}
<MudDivider Class="my-4" />
<StudentToggleSelector Students="@_students"
@bind-SelectedStudents="_selectedStudents"
Title="Students"
ShowFullName="true" />
<MudStack Class="mt-4">
<TeamCaptainSelector Students="@_selectedStudents"
@bind-SelectedCaptain="Team.Captain"
Title="Captain" />
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mt-3">@_errorMessage</MudAlert>
}
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="students">Back</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Add" OnClick="AddTeam">Add</MudButton>
</EditForm>
@code {
[SupplyParameterFromForm]
private Team Team { get; set; } = new();
private List<EventDefinition>? _events;
private List<Student> _students = [];
private IEnumerable<Student> _selectedStudents = [];
private string? _errorMessage;
private List<Team>? _existingTeams;
protected override async Task OnInitializedAsync()
{
_events =
await Context.Events
.OrderBy(e => e.Name)
.ToListAsync();
_students = await Context.Students.ToListAsync();
}
private async Task OnEventChanged(EventDefinition selectedEvent)
{
Team.Event = selectedEvent;
// Clear any previous error messages
_errorMessage = null;
// Get all existing teams for this event
_existingTeams = await Context.Teams
.Include(t => t.Event)
.Where(t => t.Event.Id == selectedEvent.Id)
.ToListAsync();
}
private async Task AddTeam()
{
// Clear previous error message
_errorMessage = null;
// Get current count of teams for this event
var existingTeamCount = await Context.Teams
.CountAsync(t => t.Event.Id == Team.Event.Id);
// Prohibit creation of third team
if (existingTeamCount >= 2)
{
_errorMessage = $"Cannot create a third team for {Team.Event.Name}. Maximum of 2 teams allowed.";
return;
}
// Handle automatic numbering based on event format
if (Team.Event.EventFormat == EventFormat.Individual && _selectedStudents.Count() == 1)
{
// For individual events, use student's first name as identifier
var student = _selectedStudents.First();
Team.Identifier = student.FirstName;
Team.Captain = student;
}
else if (existingTeamCount == 1)
{
// This is the second team - assign numbers
var existingTeam = await Context.Teams
.FirstOrDefaultAsync(t => t.Event.Id == Team.Event.Id);
if (existingTeam != null)
{
// Update existing team to number 1
existingTeam.Identifier = "1";
Context.Teams.Update(existingTeam);
}
// Set new team to number 2
Team.Identifier = "2";
}
else
{
// This is the first team - no number
Team.Identifier = null;
}
// Add selected students to the team
foreach (var student in _selectedStudents)
{
Team.Students.Add(student);
}
Context.Teams.Add(Team);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/teams");
}
}
+115
View File
@@ -0,0 +1,115 @@
@page "/teams/edit"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Edit Team - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Edit</MudText>
<MudText Typo="Typo.h4">Team @(Team.ToString())</MudText>
@if (Team is null)
{
<p><em>Loading...</em></p>
}
else
{
<EditForm method="post" Model="Team" OnValidSubmit="UpdateTeam" FormName="edit" Enhance>
<AntiforgeryToken />
<DataAnnotationsValidator/>
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<StudentToggleSelector Students="@_students"
@bind-SelectedStudents="_selectedStudents"
Title="Students"
ShowFullName="true" />
<MudStack Class="mt-4">
<TeamCaptainSelector Students="@_selectedStudents"
@bind-SelectedCaptain="Team.Captain"
Title="Captain" />
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="teams">Back</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Save" OnClick="UpdateTeam">Save</MudButton>
</EditForm>
}
@code {
[SupplyParameterFromQuery]
private int Id { get; set; }
[SupplyParameterFromForm]
private Team? Team { get; set; }
private IEnumerable<Student> _selectedStudents = [];
private List<Student> _students = [];
protected override async Task OnInitializedAsync()
{
Team ??= await Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.FirstOrDefaultAsync(m => m.Id == Id);
_students = await Context.Students.ToListAsync();
_selectedStudents = Team.Students.ToList();
if (Team is null)
{
NavigationManager.NavigateTo("notfound");
}
switch (Team!.Event.EventFormat)
{
case EventFormat.Individual when Team.Students.Count == 1:
Team.Captain ??= Team.Students[0];
Team.Identifier ??= Team.Captain.FirstName;
break;
}
}
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more information, see https://learn.microsoft.com/aspnet/core/blazor/forms/#mitigate-overposting-attacks.
private async Task UpdateTeam()
{
//Context.Attach(Team!).Entity = EntityState.Modified;
Team.Students.Clear();
foreach (var s in _selectedStudents)
{
Team.Students.Add(s);
}
// Update identifier for individual events
if (Team.Event.EventFormat == EventFormat.Individual && Team.Students.Count == 1)
{
Team.Captain ??= Team.Students[0];
Team.Identifier = Team.Captain.FirstName;
}
try
{
await Context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!TeamExists(Team!.Id))
{
NavigationManager.NavigateTo("notfound");
}
else
{
throw;
}
}
NavigationManager.NavigateTo("/teams");
}
private bool TeamExists(int id)
{
return Context.Teams.Any(e => e.Id == id);
}
}
@@ -0,0 +1,123 @@
@page "/teams/handout"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject IConfiguration Configuration
@inject AppDbContext Context
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Teams @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Shortname"] TSA Teams @Configuration["ChapterSettings:CompetitionYear"]</MudText>
<MudText Typo="Typo.h5" Class="mb-4">Yearly theme: Unity Through Community</MudText>
@if (_teams == null)
{
<p><em>Loading...</em></p>
}
else
{
<MudContainer>
@foreach (var studentForEvents in _students)
{
<MudContainer Class="pagebreak">
<MudText Typo="Typo.h4">@studentForEvents.Name</MudText>
@foreach (var team in studentForEvents.Teams)
{
<MudContainer Class="mt-3 mb-1 nobrk">
<MudGrid>
<MudItem xs="6">
<MudStack>
<MudItem>
<MudText Class="d-flex py-1" Typo="Typo.h5">
@team.ToString()
</MudText>
</MudItem>
@if (team.Event.RegionalEvent)
{
<MudItem>
<MudText Class="d-flex" Typo="Typo.caption"><i>Regional Event</i></MudText>
</MudItem>
}
</MudStack>
</MudItem>
<MudItem xs="2">
<MudText>
<strong>@team.Event.EventFormat</strong>
</MudText>
</MudItem>
<MudItem xs="1">
<strong>Effort</strong>: @AppIcons.LevelOfEffortIcon(@team.Event.LevelOfEffort)
</MudItem>
<MudItem xs="3">
<strong>Activity</strong>: @team.Event.SemifinalistActivity
</MudItem>
@if (team.Event.EventFormat == EventFormat.Team)
{
<MudItem xs="12">
<MudText Class="d-flex py-1" Typo="Typo.h6">
Team Members: @string.Join(", ", team.Students.OrderByDescending(e => e.Grade + e.TsaYear).Select(e => e.FirstName))
</MudText>
</MudItem>
}
<MudItem xs="12">
<MudText Class="d-flex py-1" Style="white-space:pre-wrap;">@team.Event.Description</MudText>
</MudItem>
@if (!string.IsNullOrEmpty(team.Event.Theme))
{
<MudItem xs="3">
<MudText Class="d-flex py-1">
<i>Theme for 2025-26:</i>
</MudText>
</MudItem>
<MudItem xs="8">
<MudText Class="d-flex py-1" Style="white-space:pre-wrap;">@team.Event.Theme</MudText>
</MudItem>
}
@if (!string.IsNullOrEmpty(team.Event.Documentation))
{
<MudItem xs="3">
<MudText Class="d-flex py-1">
<i>Materials:</i>
</MudText>
</MudItem>
<MudItem xs="8">
<MudText Class="d-flex py-1" Style="white-space:pre-wrap;">@team.Event.Documentation</MudText>
</MudItem>
}
</MudGrid>
</MudContainer>
<MudDivider/>
}
</MudContainer>
}
</MudContainer>
}
@code {
private Team[]? _teams;
private int _maxTeamSize;
private Student[]? _students;
protected override async Task OnInitializedAsync()
{
_teams
= await Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.OrderBy(e => e.Event.Name)
.ThenBy(e => e.Identifier)
.ToArrayAsync();
_maxTeamSize = _teams.Max(t => t.Students.Count);
_students =
await Context.Students
.Include(e => e.Teams)
.ThenInclude(e => e.Captain)
.Include(e => e.EventRankings)
.ThenInclude(e => e.EventDefinition)
.OrderBy(e => e.FirstName).ToArrayAsync();
}
}
@@ -0,0 +1,108 @@
@using Microsoft.EntityFrameworkCore
@page "/teams"
@attribute [Authorize]
@inject AppDbContext Context
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>TimeSlots</PageTitle>
<MudText Typo="Typo.h3">Teams</MudText>
<MudButton StartIcon="@Icons.Material.Filled.Create" Href="teams/create">Create New</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Assignment" Href="teams/assignment">Assignment</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Print" Href="teams/printout">Printout</MudButton>
<MudDataGrid T="Team" ServerData="ServerReload" @ref="_dataGrid" Filterable="true" RowsPerPage="35">
<Columns>
<TemplateColumn Title="Event">
<CellTemplate>
@context.Item.ToString()
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Attributes">
<CellTemplate>
<EventAttributes EventDefinition="@context.Item.Event"></EventAttributes>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Students">
<CellTemplate>
<TeamStudents Team="@context.Item"></TeamStudents>
</CellTemplate>
</TemplateColumn>
<TemplateColumn>
<CellTemplate>
<CrudActions
EditHref="@($"/teams/edit?id={context.Item!.Id}")"
DeleteOnClick="() => DeleteTeam(context.Item!)">
</CrudActions>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="Team"></MudDataGridPager>
</PagerContent>
</MudDataGrid>
@code {
MudDataGrid<Team> _dataGrid = null!;
private async Task<GridData<Team>> ServerReload(GridState<Team> state)
{
var query
= Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.ThenInclude(e => e.EventRankings)
.OrderBy(e => e.Event.Name)
.ThenBy(e => e.Identifier)
.Where(state.FilterDefinitions)
.OrderBy(state.SortDefinitions);
var totalItems = await query.CountAsync();
var pagedData = await query.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArrayAsync();
return new GridData<Team>
{
TotalItems = totalItems,
Items = pagedData
};
}
private async Task DeleteTeam(Team team)
{
//_isRowBlocked = true;
var result = await DialogService
.ShowMessageBox("Delete team",
(MarkupString)$"Are you sure want to delete <b>{team}</b>? This cannot be undone.",
yesText: "Yes",
noText: "Cancel");
if (result == true)
{
// If deleting a numbered team (1 or 2), clear the identifier of the remaining team
if (team.Identifier == "1" || team.Identifier == "2")
{
var remainingTeam = await Context.Teams
.Include(t => t.Event)
.FirstOrDefaultAsync(t => t.Event.Id == team.Event.Id && t.Id != team.Id);
if (remainingTeam != null)
{
remainingTeam.Identifier = null;
Context.Teams.Update(remainingTeam);
}
}
Context.Teams.Remove(team!);
await Context.SaveChangesAsync();
Snackbar.Add($"Delete event: Delete of Team {team}", Severity.Info);
}
//_isRowBlocked = false;
StateHasChanged();
await _dataGrid.ReloadServerData();
}
}
@@ -0,0 +1,236 @@
@page "/teams/printout"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject IConfiguration Configuration
@inject AppDbContext Context
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Teams @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Shortname"] TSA Teams @Configuration["ChapterSettings:CompetitionYear"]</MudText>
<MudText Typo="Typo.h5" Class="mb-4">Yearly theme: Unity Through Community</MudText>
<Legend></Legend>
@if (_teams == null)
{
<p><em>Loading...</em></p>
}
else
{
<MudContainer Class="mt-3 mb-1 nobrk">
<MudTable Items="_teams">
<HeaderContent>
<MudTh>Team</MudTh>
<MudTh></MudTh>
@for (var i = 0; i <= _maxTeamSize; i++)
{
<MudTh></MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd>
@context.ToString()
</MudTd>
<MudTd>
<EventAttributes EventDefinition="context.Event"></EventAttributes>
</MudTd>
@{
var students
= context.Students
.OrderByDescending(s => s == context.Captain)
.ThenBy(s => s.EventRankings.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue)
.ThenByDescending(e => e.Grade)
.ThenBy(e => e.FirstName)
.ToArray();
}
@for (var i = 0; i <= _maxTeamSize; i++)
{
var student = i < students.Length ? students[i] : null;
if (student != null)
{
var rank = student.EventRankings
.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue;
<MudTd Class="@(EventRankClass(rank))">
@student.Name @if(context?.Captain == student) {<span> (Cpt)</span>}
</MudTd>
}
else
{
<MudTd></MudTd>
}
}
<MudTd>
@if (context.Event.EventFormat == EventFormat.Team)
{
@if (context.Students.Count < context.Event.MinTeamSize)
{
<span>Min Team Size: @context.Event.MinTeamSize</span>
}
@if (context.Students.Count > context.Event.MaxTeamSize)
{
<span>Max Team Size: @context.Event.MaxTeamSize</span>
}
}
else if (context.Event.EventFormat == EventFormat.Individual
&& context.Students.Count > context.Event.ChapterEligibilityCountState)
{
<span>Max Team Count State: @context.Event.ChapterEligibilityCountState</span>
}
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
<MudContainer Class="mt-3 mb-1 nobrk pagebreak">
<MudTable Items="_students">
<HeaderContent>
<MudTh>Student</MudTh>
<MudTh>Effort</MudTh>
@for (var i = 0; i <= 5; i++)
{
<MudTh></MudTh>
}
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd>@context.Teams.Sum(e => e.Event.LevelOfEffort)</MudTd>
@{
var teams = context.Teams
.OrderBy(e =>
context.EventRankings.Find(ser => ser.EventDefinition == e.Event)?.Rank ?? int.MaxValue
).ToArray();
}
@for (var i = 0; i <= 5; i++)
{
var team = i < teams.Length ? teams[i] : null;
@if (team != null)
{
var rank = context.EventRankings
.Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue;
<MudTh Class="@(EventRankClass(rank))">
@team.ToString()
<EventAttributes EventDefinition="team.Event"></EventAttributes>
@if (rank == int.MaxValue)
{
<span>❔</span>
}
</MudTh>
}
else
{
<MudTh></MudTh>
}
}
<MudTd>
@if (!context.Teams.Select(e => e.Event).Any(re => re.OnSiteActivity))
{
<span>No On-Site Activity</span>
}
@if (!context.Teams.Select(e => e.Event).Any(re => re.RegionalEvent))
{
<span>No Regional Event</span>
}
</MudTd>
</RowTemplate>
</MudTable>
</MudContainer>
<MudContainer Class="mt-3 mb-1 nobrk pagebreak">
<MudTable Items="_students">
<HeaderContent>
<MudTh>Student</MudTh>
<MudTh>Grade, TSA Year</MudTh>
<MudTh>Level of Effort</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Name</MudTd>
<MudTd>@AppIcons.GetOrdinal(context.Grade), @context.TsaYear</MudTd>
<MudTd>@context.Teams.Sum(e => e.Event.LevelOfEffort)
@if (!context.Teams.Select(e => e.Event).Any(re => re.OnSiteActivity))
{
<span>No On-Site Activity</span>
}
@if (!context.Teams.Select(e => e.Event).Any(re => re.RegionalEvent))
{
<span>No Regional Event</span>
}
</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTr>
<MudTable Items="@context.Teams.OrderBy(e =>
context.EventRankings.Find(ser => ser.EventDefinition == e.Event)?.Rank ?? int.MaxValue
)" Context="team">
<RowTemplate>
@{ var rank = context.EventRankings
.Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue; }
<MudTd Class="@(EventRankClass(rank))">
@team.ToString()
@AppIcons.EventEffort(team.Event)
@AppIcons.EventAttributes(team.Event)
@if (rank == int.MaxValue)
{
<span>❔</span>
}
</MudTd>
<MudTd>@string.Join(", ", team.Students.Where(e => e != context).Select(e => e.FirstName))</MudTd>
<MudTd></MudTd>
</RowTemplate>
</MudTable>
</MudTr>
</ChildRowContent>
</MudTable>
</MudContainer>
}
@code {
private Team[]? _teams;
private int _maxTeamSize;
private Student[]? _students;
private bool _rankColorEnabled;
private string EventRankClass(int rank)
{
if (!_rankColorEnabled)
return "";
return "event-rank-" + rank;
}
protected override async Task OnInitializedAsync()
{
_teams
= await Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.OrderBy(e => e.Event.Name)
.ThenBy(e => e.Identifier)
.ToArrayAsync();
_maxTeamSize = _teams.Max(t => t.Students.Count);
_students =
await Context.Students
.Include(e => e.Teams)
.ThenInclude(e => e.Captain)
.Include(e => e.EventRankings)
.ThenInclude(e => e.EventDefinition)
.OrderBy(e => e.FirstName).ToArrayAsync();
}
}