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
@@ -1,62 +0,0 @@
@page "/events/create"
@attribute [Authorize]
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Create Event - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Create</MudText>
<MudText Typo="Typo.h4">Event</MudText>
<MudDivider />
<EditForm Model="EventDefinition" OnValidSubmit="OnValidSubmit" Enhance>
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<MudTextField T="string" Label="Event Name" @bind-Value="EventDefinition.Name" For="@(() => EventDefinition.Name)"></MudTextField>
<MudTextField T="string" Label="Short Name" @bind-Value="EventDefinition.ShortName" For="@(() => EventDefinition.ShortName)"></MudTextField>
<MudText Typo="Typo.subtitle2" Class="mt-4 mb-2">Format</MudText>
<MudRadioGroup T="EventFormat" @bind-Value="@EventDefinition.EventFormat" For="@(() => EventDefinition.EventFormat)">
@foreach (EventFormat format in Enum.GetValues(typeof(EventFormat)))
{
<MudRadio T="EventFormat" Value="@format">@format.ToString()</MudRadio>
}
</MudRadioGroup>
<MudTextField T="string" Label="Description" AutoGrow="true" @bind-Value="EventDefinition.Description" For="@(() => EventDefinition.Description)" Class="mt-4"></MudTextField>
<MudTextField T="string" Label="Theme" AutoGrow="true" @bind-Value="EventDefinition.Theme" For="@(() => EventDefinition.Theme)"></MudTextField>
<MudTextField T="string" Label="Documentation" @bind-Value="EventDefinition.Documentation" For="@(() => EventDefinition.Documentation)"></MudTextField>
<MudNumericField T="int?" Label="Level of Effort" @bind-Value="EventDefinition.LevelOfEffort" For="@(() => EventDefinition.LevelOfEffort)"></MudNumericField>
<MudDivider></MudDivider>
<MudTextField T="string" Label="Nationals Eligibility" @bind-Value="EventDefinition.Eligibility" For="@(() => EventDefinition.Eligibility)"></MudTextField>
<MudNumericField T="int" Label="Minimum Team Size" @bind-Value="EventDefinition.MinTeamSize" For="@(() => EventDefinition.MinTeamSize)"></MudNumericField>
<MudNumericField T="int" Label="Maxiumum Team Size" @bind-Value="EventDefinition.MaxTeamSize" For="@(() => EventDefinition.MaxTeamSize)"></MudNumericField>
<MudNumericField T="int" Label="Team Count at Regionals" @bind-Value="EventDefinition.ChapterEligibilityCountRegionals" For="@(() => EventDefinition.ChapterEligibilityCountRegionals)"></MudNumericField>
<MudNumericField T="int" Label="Team Count at State" @bind-Value="EventDefinition.ChapterEligibilityCountState" For="@(() => EventDefinition.ChapterEligibilityCountState)"></MudNumericField>
<MudDivider></MudDivider>
<MudTextField T="string" Label="Semifinalist Activity" @bind-Value="EventDefinition.SemifinalistActivity" For="@(() => EventDefinition.SemifinalistActivity)"></MudTextField>
<MudCheckBox T="bool" Label="On Site Activity" @bind-Value="EventDefinition.OnSiteActivity" For="@(() => EventDefinition.OnSiteActivity)"></MudCheckBox>
<MudCheckBox T="bool" Label="Requires Presubmission" @bind-Value="EventDefinition.Presubmission" For="@(() => EventDefinition.Presubmission)"></MudCheckBox>
</MudPaper>
</MudItem>
</MudGrid>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="events">Back</MudButton>
<MudButton ButtonType="ButtonType.Submit" StartIcon="@Icons.Material.Filled.Save">Save</MudButton>
</EditForm>
@code {
[SupplyParameterFromForm]
private EventDefinition EventDefinition { get; set; } = new();
private void OnValidSubmit()
{
context.Events.Add(EventDefinition);
context.SaveChanges();
NavigationManager.NavigateTo("/events");
}
}
@@ -1,105 +0,0 @@
@page "/events/details"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Event Details - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Details</MudText>
<MudText Typo="Typo.h4">Event Definition</MudText>
<MudDivider />
@if (eventdefinition is null)
{
<MudText><em>Loading...</em></MudText>
}
else
{
<MudPaper Class="pa-4 mt-4">
<MudGrid>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Name</MudText>
<MudText>@eventdefinition.Name</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Short Name</MudText>
<MudText>@eventdefinition.ShortName</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Event Format</MudText>
<MudText>@eventdefinition.EventFormat</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Min Team Size</MudText>
<MudText>@eventdefinition.MinTeamSize</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Max Team Size</MudText>
<MudText>@eventdefinition.MaxTeamSize</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Chapter Eligibility Count (State)</MudText>
<MudText>@eventdefinition.ChapterEligibilityCountState</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Regional Event</MudText>
<MudText>@eventdefinition.RegionalEvent</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Presubmission Required</MudText>
<MudText>@eventdefinition.Presubmission</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Level of Effort</MudText>
<MudText>@eventdefinition.LevelOfEffort</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2">Semifinalist Activity</MudText>
<MudText>@eventdefinition.SemifinalistActivity</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2">Notes</MudText>
<MudText>@eventdefinition.Notes</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2">Documentation</MudText>
<MudText>@eventdefinition.Documentation</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2">Eligibility</MudText>
<MudText>@eventdefinition.Eligibility</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2">Theme</MudText>
<MudText>@eventdefinition.Theme</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2">Description</MudText>
<MudText>@eventdefinition.Description</MudText>
</MudItem>
</MudGrid>
</MudPaper>
<div class="mt-4">
<MudButton StartIcon="@Icons.Material.Filled.Edit" Href="@($"/events/edit?id={eventdefinition.Id}")" Variant="Variant.Filled" Color="Color.Primary">Edit</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="/events" Variant="Variant.Text">Back to List</MudButton>
</div>
}
@code {
private EventDefinition? eventdefinition;
[SupplyParameterFromQuery]
private int Id { get; set; }
protected override async Task OnInitializedAsync()
{
eventdefinition = await context.Events.FirstOrDefaultAsync(m => m.Id == Id);
if (eventdefinition is null)
{
NavigationManager.NavigateTo("notfound");
}
}
}
@@ -1,99 +0,0 @@
@page "/events/edit"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Edit Event - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Edit</MudText>
<MudText Typo="Typo.h4">Event</MudText>
<MudDivider />
<EditForm Model="EventDefinition" OnValidSubmit="OnValidSubmit" Enhance>
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<MudTextField T="string" Label="Event Name" @bind-Value="EventDefinition.Name" For="@(() => EventDefinition.Name)"></MudTextField>
<MudTextField T="string" Label="Short Name" @bind-Value="EventDefinition.ShortName" For="@(() => EventDefinition.ShortName)"></MudTextField>
<MudText Typo="Typo.subtitle2" Class="mt-4 mb-2">Format</MudText>
<MudRadioGroup T="EventFormat" @bind-Value="@EventDefinition.EventFormat" For="@(() => EventDefinition.EventFormat)">
@foreach (EventFormat format in Enum.GetValues(typeof(EventFormat)))
{
<MudRadio T="EventFormat" Value="@format">@format.ToString()</MudRadio>
}
</MudRadioGroup>
<MudTextField T="string" Label="Description" AutoGrow="true" @bind-Value="EventDefinition.Description" For="@(() => EventDefinition.Description)" Class="mt-4"></MudTextField>
<MudTextField T="string" Label="Theme" AutoGrow="true" @bind-Value="EventDefinition.Theme" For="@(() => EventDefinition.Theme)"></MudTextField>
<MudTextField T="string" Label="Documentation" @bind-Value="EventDefinition.Documentation" For="@(() => EventDefinition.Documentation)"></MudTextField>
<MudNumericField T="int?" Label="Level of Effort" @bind-Value="EventDefinition.LevelOfEffort" For="@(() => EventDefinition.LevelOfEffort)"></MudNumericField>
<MudDivider></MudDivider>
<MudTextField T="string" Label="Nationals Eligibility" @bind-Value="EventDefinition.Eligibility" For="@(() => EventDefinition.Eligibility)"></MudTextField>
<MudNumericField T="int" Label="Minimum Team Size" @bind-Value="EventDefinition.MinTeamSize" For="@(() => EventDefinition.MinTeamSize)"></MudNumericField>
<MudNumericField T="int" Label="Maxiumum Team Size" @bind-Value="EventDefinition.MaxTeamSize" For="@(() => EventDefinition.MaxTeamSize)"></MudNumericField>
<MudNumericField T="int" Label="Team Count at Regionals" @bind-Value="EventDefinition.ChapterEligibilityCountRegionals" For="@(() => EventDefinition.ChapterEligibilityCountRegionals)"></MudNumericField>
<MudNumericField T="int" Label="Team Count at State" @bind-Value="EventDefinition.ChapterEligibilityCountState" For="@(() => EventDefinition.ChapterEligibilityCountState)"></MudNumericField>
<MudDivider></MudDivider>
<MudTextField T="string" Label="Semifinalist Activity" @bind-Value="EventDefinition.SemifinalistActivity" For="@(() => EventDefinition.SemifinalistActivity)"></MudTextField>
<MudCheckBox T="bool" Label="On Site Activity" @bind-Value="EventDefinition.OnSiteActivity" For="@(() => EventDefinition.OnSiteActivity)"></MudCheckBox>
<MudCheckBox T="bool" Label="Requires Presubmission" @bind-Value="EventDefinition.Presubmission" For="@(() => EventDefinition.Presubmission)"></MudCheckBox>
</MudPaper>
</MudItem>
</MudGrid>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="events">Back</MudButton>
<MudButton ButtonType="ButtonType.Submit" StartIcon="@Icons.Material.Filled.Save">Save</MudButton>
</EditForm>
@code {
[SupplyParameterFromQuery]
private int Id { get; set; }
[SupplyParameterFromForm]
private EventDefinition? EventDefinition { get; set; }
protected override async Task OnInitializedAsync()
{
EventDefinition ??= await context.Events.FirstOrDefaultAsync(m => m.Id == Id);
if (EventDefinition is null)
{
NavigationManager.NavigateTo("notfound");
}
}
// 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 void OnValidSubmit()
{
context.Attach(EventDefinition!).State = EntityState.Modified;
try
{
context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!EventDefinitionExists(EventDefinition!.Id))
{
NavigationManager.NavigateTo("notfound");
}
else
{
throw;
}
}
NavigationManager.NavigateTo("/events");
}
private bool EventDefinitionExists(int id)
{
return context.Events.Any(e => e.Id == id);
}
}
@@ -1,89 +0,0 @@
@page "/events"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>Events - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Events</MudText>
<MudButton StartIcon="@Icons.Material.Filled.Create" Href="events/create">Create New</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Print" Href="events/printout">Printable Descriptions</MudButton>
<MudDataGrid T="EventDefinition" ServerData="ServerReload" @ref="_dataGrid" Filterable="true" RowsPerPage="50" >
<Columns>
<PropertyColumn Property="@(e => e.Name)" Title="Event Name" Sortable="true" />
<PropertyColumn Property="@(e => e.EventFormat)" Title="Event Format" />
<TemplateColumn Title="Team Size" CellStyle="white-space:nowrap">
<CellTemplate>
<MudTooltip Text="@context.Item.Eligibility">
[@context.Item.MinTeamSize&nbsp;-&nbsp;@context.Item.MaxTeamSize]
</MudTooltip>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="@(e => e.ChapterEligibilityCountState)" Title="State#" />
<TemplateColumn Title="Attributes" Sortable="false">
<CellTemplate>
<EventAttributes EventDefinition="context.Item"></EventAttributes>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Property="@(e => e.LevelOfEffort)" Title="Level of Effort" />
<TemplateColumn>
<CellTemplate>
<CrudActions DetailsHref="@($"/events/details?id={context.Item!.Id}")"
EditHref="@($"/events/edit?id={context.Item!.Id}")"
DeleteOnClick="() => DeleteEventDefinition(context.Item!)">
</CrudActions>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="EventDefinition"></MudDataGridPager>
</PagerContent>
</MudDataGrid>
@code {
MudDataGrid<EventDefinition> _dataGrid = null!;
private async Task<GridData<EventDefinition>> ServerReload(GridState<EventDefinition> state)
{
var query = Context.Events.OrderBy(e => e.Name).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<EventDefinition>
{
TotalItems = totalItems,
Items = pagedData
};
}
private async Task DeleteEventDefinition(EventDefinition evt)
{
//_isRowBlocked = true;
var result = await DialogService
.ShowMessageBox("Delete Event",
(MarkupString)$"Are you sure want to delete <b>{evt.Name}</b>? This cannot be undone.",
yesText:"Yes",
noText:"Cancel");
if (result == true)
{
Context.Events.Remove(evt!);
await Context.SaveChangesAsync();
Snackbar.Add($"Delete event: Delete of Event {evt.Name}", Severity.Info);
}
//_isRowBlocked = false;
StateHasChanged();
await _dataGrid.ReloadServerData();
}
}
@@ -1,179 +0,0 @@
@using Microsoft.EntityFrameworkCore
@attribute [Authorize]
@page "/events/printout"
@inject IConfiguration Configuration
@inject AppDbContext Context
<PageTitle>TSA Events @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<MudText Typo="Typo.h3">TSA Events @Configuration["ChapterSettings:CompetitionYear"]</MudText>
<MudText Typo="Typo.h5" Class="mb-4">Yearly theme: Unity Through Community</MudText>
@if (_events == null)
{
<p><em>Loading...</em></p>
}
else
{
<MudContainer>
@foreach (var evt in _events)
{
<MudContainer Class="mt-3 mb-1 nobrk">
<MudGrid>
<MudItem xs="4">
<MudStack>
<MudItem>
<MudText Class="d-flex py-1" Typo="Typo.h5">@evt.Name</MudText>
</MudItem>
@if (evt.RegionalEvent)
{
<MudItem>
<MudText Class="d-flex" Typo="Typo.caption"><i>Regional Event</i></MudText>
</MudItem>
}
</MudStack>
</MudItem>
<MudItem xs="2">
<MudText>
@if (evt.EventFormat is EventFormat.Team)
{
<strong>@evt.EventFormat</strong>
<br/>
<p>Size: <strong>@evt.TeamSize</strong></p>
}
else
{
<strong>@evt.EventFormat</strong>
}
</MudText>
</MudItem>
<MudItem xs="3">
Eligibility: @evt.Eligibility
</MudItem>
<MudItem xs="1">
<strong> Effort</strong>: @evt.LevelOfEffort
</MudItem>
<MudItem xs="2">
<strong>Activity</strong>: @evt.SemifinalistActivity
</MudItem>
<MudItem xs="12">
<MudText Class="d-flex py-1" Style="white-space:pre-wrap;">@evt.Description</MudText>
</MudItem>
@if (!string.IsNullOrEmpty(evt.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;">@evt.Theme</MudText>
</MudItem>
}
@if (!string.IsNullOrEmpty(evt.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;">@evt.Documentation</MudText>
</MudItem>
}
</MudGrid>
</MudContainer>
<MudDivider />
}
</MudContainer>
<MudContainer>
@foreach (var evt in _events)
{
<MudContainer Class="mt-3 mb-1 nobrk">
<MudGrid>
<MudItem xs="4">
<MudStack>
<MudItem>
<MudText Class="d-flex py-1" Typo="Typo.h5">@evt.Name</MudText>
</MudItem>
@if (evt.RegionalEvent)
{
<MudItem>
<MudText Class="d-flex" Typo="Typo.caption"><i>Regional Event</i></MudText>
</MudItem>
}
</MudStack>
</MudItem>
<MudItem xs="2">
<MudText>
@if (evt.EventFormat is EventFormat.Team)
{
<strong>@evt.EventFormat</strong>
<br />
<p>Size: <strong>@evt.TeamSize</strong></p>
}
else
{
<strong>@evt.EventFormat</strong>
}
</MudText>
</MudItem>
<MudItem xs="3">
Eligibility: @evt.Eligibility
</MudItem>
<MudItem xs="1">
<strong> Effort</strong>: @evt.LevelOfEffort
</MudItem>
<MudItem xs="2">
<strong>Activity</strong>: @evt.SemifinalistActivity
</MudItem>
<MudItem xs="12">
<MudText Class="d-flex py-1" Style="white-space:pre-wrap;">@evt.Description</MudText>
</MudItem>
@if (!string.IsNullOrEmpty(evt.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;">@evt.Theme</MudText>
</MudItem>
}
@if (!string.IsNullOrEmpty(evt.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;">@evt.Documentation</MudText>
</MudItem>
}
</MudGrid>
</MudContainer>
<MudDivider />
}
</MudContainer>
}
@code {
private EventDefinition[]? _events;
protected override async Task OnInitializedAsync()
{
_events = await Context.Events.OrderBy(e => e.Name).ToArrayAsync();
}
}
@@ -1,340 +0,0 @@
@page "/meeting-schedule"
@attribute [Authorize]
@using System.Text
@using Core.Calculation
@using Microsoft.EntityFrameworkCore
@inject IConfiguration Configuration
@inject AppDbContext Context
@inject ClipboardService ClipboardService
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</MudText>
<MudPaper Class="pa-4 mt-5">
<MudGrid>
<MudItem xs="7" sm="8" lg="9">
<MudText Typo="Typo.h4">Time Slots</MudText>
<MudPaper Class="pa-2 ma-2" Elevation="3">
<MudGrid>
<MudItem xs="6" sm="3" lg="2">
<MudNumericField @bind-Value="_parameters.TimeSlots"
Label="Time Slots" Min="1" Max="4">
</MudNumericField>
</MudItem>
<MudFlexBreak/>
<MudItem xs="12" sm="6" lg="4">
<MudTooltip Text="Schedule teams with Level of Effort >= 3" Inline="false">
<MudButton Variant="Variant.Outlined" OnClick="AddHighLevelOfEffort" FullWidth="true">Add High Effort</MudButton>
</MudTooltip>
</MudItem>
<MudItem xs="12" sm="6" lg="4">
<MudButton Variant="Variant.Outlined" OnClick="AddRegionals" FullWidth="true">Add Regionals</MudButton>
</MudItem>
<MudItem xs="12" sm="6" lg="4">
<MudButton Variant="Variant.Outlined" OnClick="RemoveIndividual" FullWidth="true">Remove Individual</MudButton>
</MudItem>
<MudItem xs="12" sm="6" lg="4">
<MudButton Variant="Variant.Outlined" OnClick="RemoveLowLevelOfEffort" FullWidth="true">Remove Low Effort</MudButton>
</MudItem>
<MudItem xs="12" sm="6" lg="4">
<MudButton Variant="Variant.Outlined" OnClick="Invert" FullWidth="true">Invert</MudButton>
</MudItem>
<MudItem xs="12" sm="6" lg="4">
<MudButton Variant="Variant.Outlined" Color="Color.Warning" OnClick="Reset" FullWidth="true">Reset</MudButton>
</MudItem>
<MudItem xs="12">
<MudButton Variant="Variant.Filled" Class="ma-3" OnClick="Solve" Color="Color.Primary" Disabled="@_isSolving">Solve</MudButton>
<MudTooltip Text="Copy to Clipboard">
<MudIconButton OnClick="CopyToClipboard" Icon="@Icons.Material.Filled.ContentCopy"></MudIconButton>
</MudTooltip>
</MudItem>
</MudGrid>
</MudPaper>
<MudTable T="TeamScheduleTimeSlot" ServerData="SolveSchedule" @ref="_solutionData">
<HeaderContent>
</HeaderContent>
<RowTemplate>
<MudTd>
<MudGrid>
<MudItem xs="12" lg="6">
<ScheduledTeamsList TimeSlotName="@context.Name"
Teams="@context.Teams"
ScheduledTeams="@_scheduledTeams"
AbsentStudents="@_absentStudents"
StudentHasOverlaps="@context.StudentHasOverlaps"
OnToggleTeam="@ToggleRequiredTeam" />
</MudItem>
<MudItem xs="12" lg="6">
<UnscheduledStudentsList UnscheduledStudents="@context.UnscheduledStudents"
AbsentStudents="@_absentStudents"
ScheduledTeams="@_scheduledTeams"
PossibleAdditions="@_possibleAdditions"
UnassignedTeams="@_solution.StudentUnassignedTeams"
OnToggleTeam="@ToggleRequiredTeam" />
</MudItem>
</MudGrid>
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem xs="5" sm="4" lg="3">
<MudStack>
<StudentTextBoxSelector Students="@_students"
@bind-SelectedStudents="_absentStudents"
Title="Absent Students"
Label="Search for absent students"
ShowFullName="true"/>
<MudDivider Class="my-4"/>
<TeamToggleSelector Teams="@_teams"
@bind-SelectedTeams="_scheduledTeams"
Title="Scheduled Teams"
ShowEventAttributes="true" />
</MudStack>
</MudItem>
</MudGrid>
</MudPaper>
@code {
private Team[]? _teams;
private Student[]? _students;
MudTable<TeamScheduleTimeSlot> _solutionData;
private TeamSchedulerSolution _solution;
private TeamSchedulerOptions _parameters;
bool _isSolving;
private IEnumerable<Team> _scheduledTeams = [];
private IEnumerable<Student> _absentStudents = [];
private IEnumerable<Team> _possibleAdditions = [];
private void AddRegionals()
{
_scheduledTeams
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
}
private void AddHighLevelOfEffort()
{
_scheduledTeams
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
}
private void RemoveIndividual()
{
_scheduledTeams
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
}
private void RemoveLowLevelOfEffort()
{
_scheduledTeams
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
}
private void Invert()
{
var rt = _scheduledTeams.ToArray();
_scheduledTeams
= _teams.Where(t => !rt.Contains(t));
}
private void Reset()
{
_scheduledTeams = [];
}
private void ToggleRequiredTeam(Team unassignedTeam)
{
if (_scheduledTeams.Contains(unassignedTeam))
_scheduledTeams = _scheduledTeams.Where(t => t != unassignedTeam);
else
{
_scheduledTeams = _scheduledTeams.Concat(new[] { unassignedTeam });
}
}
protected override async Task OnInitializedAsync()
{
_parameters =
new TeamSchedulerOptions(
2,
mustIncludeEvents:
[
// "Medical Technology", "Electrical Applications" , "RegionalTeam",
// ,"Dragster", "Flight"
],
extended:
[
// "Invention", "Construction Challenge", "Mechanical", "Mass", "Micro"
//"STEM"
//"Community", "Vlogging"// "Microcontroller"
],
omittedEvents:
[
// "Vlogging", "Junior", "Community Service Video", "Digital Photography",
// "STEM"
//"Leadership",// "Electrical", //"Construction"
// "Forensic",
//"CAD"
//"I&I Team 1", "I&I Team 2"//, "Website Design",
],
absentStudents:
[
]
);
_teams
= await Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.OrderBy(e => e.Event.Name)
.ThenBy(e => e.Identifier)
.ToArrayAsync();
_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();
}
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
{
_isSolving = true;
// Check if there are any teams to schedule
if (!_scheduledTeams.Any())
{
_isSolving = false;
_solution = new TeamSchedulerSolution([], [], "No teams selected");
return new TableData<TeamScheduleTimeSlot> { Items = [] };
}
// Filter out absent students
var availableStudents = _students.Where(s => !_absentStudents.Contains(s)).ToArray();
// Check if there are any available students
if (availableStudents.Length == 0)
{
_isSolving = false;
_solution = new TeamSchedulerSolution([], [], "No available students");
return new TableData<TeamScheduleTimeSlot> { Items = [] };
}
// Update parameters with absent student names
_parameters.AbsentStudents = _absentStudents.Select(s => s.FirstNameLastName).ToArray();
var teamScheduler = new TeamScheduler(_scheduledTeams, _parameters.TimeSlots, availableStudents);
_solution = teamScheduler.Solve();
// Try recommendation strategies in priority order
var scheduler = new UnassignedStudentScheduler(_teams, _solution.TimeSlots);
var strategies = new[]
{
UnassignedScheduleStrategy.LevelOfEffort,
UnassignedScheduleStrategy.BiggestGroup,
UnassignedScheduleStrategy.AnyNotMeetingAlready,
UnassignedScheduleStrategy.IndividualEvents
};
_possibleAdditions = strategies
.Select(strategy => scheduler.ScheduleStrategy(strategy))
.FirstOrDefault(result => result.Any()) ?? [];
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
_isSolving = false;
return new TableData<TeamScheduleTimeSlot> { Items = _solution.TimeSlots };
}
private void Solve()
{
_solutionData.ReloadServerData();
}
async Task CopyToClipboard()
{
var sb = new StringBuilder();
foreach (var timeslot in _solution.TimeSlots)
{
AppendScheduledTeams(sb, timeslot);
AppendUnscheduledStudents(sb, timeslot);
sb.Append(Environment.NewLine);
}
try
{
await ClipboardService.WriteTextAsync(sb.ToString());
}
catch
{
Console.WriteLine("Cannot write text to clipboard");
}
}
private void AppendScheduledTeams(StringBuilder sb, TeamScheduleTimeSlot timeslot)
{
foreach (var scheduledTeam in timeslot.Teams.OrderBy(e => e.ToString()))
{
var teamName = scheduledTeam.ToString();
if (scheduledTeam.Event.EventFormat is EventFormat.Individual)
{
sb.Append(teamName);
}
else
{
var studentsList = FormatStudentList(scheduledTeam, timeslot);
sb.Append($"{teamName} - {studentsList}");
}
sb.Append(Environment.NewLine);
}
}
private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot)
{
return string.Join(", ",
team.Students
.OrderBy(e => e == team.Captain)
.ThenBy(e => e.FirstName)
.Select(e => FormatStudentName(e, timeslot)));
}
private string FormatStudentName(Student student, TeamScheduleTimeSlot timeslot)
{
var name = student.FirstName;
if (timeslot.StudentHasOverlaps(student))
name += "*";
if (_absentStudents.Contains(student))
name += " (absent)";
return name;
}
private void AppendUnscheduledStudents(StringBuilder sb, TeamScheduleTimeSlot timeslot)
{
if (!timeslot.UnscheduledStudents.Any())
return;
sb.Append("--Unscheduled");
sb.Append(Environment.NewLine);
foreach (var student in timeslot.UnscheduledStudents)
{
var studentName = student.FirstName;
if (_absentStudents.Contains(student))
studentName += " (absent)";
var unassignedTeams = _solution.StudentUnassignedTeams(student);
var teamsList = string.Join(", ", unassignedTeams.Select(e => e.ToString()));
sb.Append($"{studentName} - {teamsList}");
sb.Append(Environment.NewLine);
}
}
}
@@ -1,62 +0,0 @@
@using Core.Calculation
<MudStack>
<MudText Typo="Typo.h6">@TimeSlotName</MudText>
@foreach (var team in Teams.OrderBy(e => e.ToString()))
{
var removed = !ScheduledTeams.Contains(team);
<MudLink Typo="Typo.body1"
Class="d-flex align-center"
Color="Color.Default"
OnClick="@(() => OnToggleTeam.InvokeAsync(team))">
<MudIcon Icon="@Icons.Material.Filled.Clear"
Size="Size.Small"
Class="@(removed ? "" : "d-none")">
</MudIcon>
@team -
@foreach (var student in team.Students)
{
var overlap = StudentHasOverlaps(student);
var isAbsent = AbsentStudents.Contains(student);
var color = overlap ? Color.Warning : Color.Default;
var suffix = GetStudentSuffix(overlap, isAbsent);
if (student != team.Students.First())
{
<MudText>, </MudText>
}
<MudText Typo="Typo.body2" Color="@color">
&nbsp;@student.FirstName@suffix
</MudText>
}
</MudLink>
}
</MudStack>
@code {
[Parameter]
public string TimeSlotName { get; set; } = string.Empty;
[Parameter]
public IEnumerable<Team> Teams { get; set; } = [];
[Parameter]
public IEnumerable<Team> ScheduledTeams { get; set; } = [];
[Parameter]
public IEnumerable<Student> AbsentStudents { get; set; } = [];
[Parameter]
public Func<Student, bool> StudentHasOverlaps { get; set; } = null!;
[Parameter]
public EventCallback<Team> OnToggleTeam { get; set; }
private string GetStudentSuffix(bool overlap, bool isAbsent)
{
var suffix = overlap ? "*" : "";
suffix += isAbsent ? " (absent)" : "";
return suffix;
}
}
@@ -1,57 +0,0 @@
@using Core.Calculation
@if (UnscheduledStudents.Any())
{
<MudText Typo="Typo.body1" HtmlTag="strong">Unscheduled</MudText>
<MudStack>
@foreach (var student in UnscheduledStudents)
{
var isAbsent = AbsentStudents.Contains(student);
<MudItem>
<MudText Typo="Typo.body1" HtmlTag="i">
@student.FirstName@(isAbsent ? " (absent)" : "")&nbsp;
</MudText>
@foreach (var unassignedTeam in UnassignedTeams(student))
{
var isPossibleAddition = PossibleAdditions.Contains(unassignedTeam, new TeamIdComparer());
var isScheduled = ScheduledTeams.Contains(unassignedTeam);
var color = isPossibleAddition ? Color.Success : Color.Default;
if (unassignedTeam != UnassignedTeams(student).First())
{
<span>, </span>
}
<MudLink Typo="Typo.body2"
Color="@color"
OnClick="@(() => OnToggleTeam.InvokeAsync(unassignedTeam))">
<MudIcon Icon="@Icons.Material.Filled.Check"
Size="Size.Small"
Class="@(isScheduled ? "" : "d-none")">
</MudIcon>
@unassignedTeam
</MudLink>
}
</MudItem>
}
</MudStack>
}
@code {
[Parameter]
public IEnumerable<Student> UnscheduledStudents { get; set; } = [];
[Parameter]
public IEnumerable<Student> AbsentStudents { get; set; } = [];
[Parameter]
public IEnumerable<Team> ScheduledTeams { get; set; } = [];
[Parameter]
public IEnumerable<Team> PossibleAdditions { get; set; } = [];
[Parameter]
public Func<Student, IEnumerable<Team>> UnassignedTeams { get; set; } = null!;
[Parameter]
public EventCallback<Team> OnToggleTeam { get; set; }
}
@@ -1,43 +0,0 @@
@page "/students/create"
@attribute [Authorize]
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Create Student - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Create</MudText>
<MudText Typo="Typo.h4">Student</MudText>
<MudDivider />
<EditForm Model="Student" OnValidSubmit="OnValidSubmit" Enhance>
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<MudTextField T="string" Label="First Name" @bind-Value="Student.FirstName" For="@(() => Student.FirstName)"></MudTextField>
<MudTextField T="string" Label="Last Name" @bind-Value="Student.LastName" For="@(() => Student.LastName)"></MudTextField>
<MudTextField T="string" Label="Email Adress" @bind-Value="Student.Email" For="@(() => Student.Email)"></MudTextField>
<MudTextField T="string" Label="Phone Number" @bind-Value="Student.PhoneNumber" For="@(() => Student.PhoneNumber)"></MudTextField>
<MudTextField T="int" Label="Grade" @bind-Value="Student.Grade" For="@(() => Student.Grade)"></MudTextField>
<MudTextField T="int" Label="TSA Year" @bind-Value="Student.TsaYear" For="@(() => Student.TsaYear)"></MudTextField>
</MudPaper>
</MudItem>
</MudGrid>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="students">Back</MudButton>
<MudButton ButtonType="ButtonType.Submit" StartIcon="@Icons.Material.Filled.Save">Save</MudButton>
</EditForm>
@code {
[SupplyParameterFromForm]
private Student Student { get; set; } = new() { TsaYear = 1 };
private void OnValidSubmit(EditContext context)
{
Context.Students.Add(Student);
Context.SaveChanges();
NavigationManager.NavigateTo("/students");
}
}
@@ -1,87 +0,0 @@
@page "/students/details"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using Core.Entities
@using Data
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Student Details - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Details</MudText>
<MudText Typo="Typo.h4">Student</MudText>
<MudDivider />
@if (student is null)
{
<MudText><em>Loading...</em></MudText>
}
else
{
<MudPaper Class="pa-4 mt-4">
<MudGrid>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">First Name</MudText>
<MudText>@student.FirstName</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Last Name</MudText>
<MudText>@student.LastName</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Grade</MudText>
<MudText>@student.Grade</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Email</MudText>
<MudText>@student.Email</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Phone Number</MudText>
<MudText>@student.PhoneNumber</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">TSA Year</MudText>
<MudText>@student.TsaYear</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">State ID</MudText>
<MudText>@student.StateId</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Regional ID</MudText>
<MudText>@student.RegionalId</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">National ID</MudText>
<MudText>@student.NationalId</MudText>
</MudItem>
<MudItem xs="12" sm="6" md="4">
<MudText Typo="Typo.subtitle2">Officer Role</MudText>
<MudText>@student.OfficerRole</MudText>
</MudItem>
</MudGrid>
</MudPaper>
<div class="mt-4">
<MudButton StartIcon="@Icons.Material.Filled.Edit" Href="@($"/students/edit?id={student.Id}")" Variant="Variant.Filled" Color="Color.Primary">Edit</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="/students" Variant="Variant.Text">Back to List</MudButton>
</div>
}
@code {
private Student? student;
[SupplyParameterFromQuery]
private int Id { get; set; }
protected override async Task OnInitializedAsync()
{
student = await context.Students.FirstOrDefaultAsync(m => m.Id == Id);
if (student is null)
{
NavigationManager.NavigateTo("notfound");
}
}
}
@@ -1,102 +0,0 @@
@page "/students/edit"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Edit Student - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Edit</MudText>
<MudText Typo="Typo.h4">Student@(@Student == null ? "" : $" ({Student.Name})")</MudText>
@if (Student is null)
{
<p><em>Loading...</em></p>
}
else
{
/* https://www.mudblazor.com/components/form */
/* https://medium.com/@husainalbar/applying-mudblazor-for-crud-operations-in-our-blazor-project-a343037a52ef */
<EditForm method="post" Model="Student" OnValidSubmit="UpdateStudent" FormName="edit" Enhance>
<AntiforgeryToken />
<DataAnnotationsValidator/>
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<MudTextField T="string" Label="First Name" @bind-Value="Student.FirstName" For="@(() => Student.FirstName)"></MudTextField>
<MudTextField T="string" Label="Last Name" @bind-Value="Student.LastName" For="@(() => Student.LastName)"></MudTextField>
<MudTextField T="string" Label="Email Adress" @bind-Value="Student.Email" For="@(() => Student.Email)"></MudTextField>
<MudTextField T="string" Label="Phone Number" @bind-Value="Student.PhoneNumber" For="@(() => Student.PhoneNumber)"></MudTextField>
<MudTextField T="int" Label="Grade" @bind-Value="Student.Grade" For="@(() => Student.Grade)"></MudTextField>
<MudTextField T="int" Label="TSA Year" @bind-Value="Student.TsaYear" For="@(() => Student.TsaYear)"></MudTextField>
<MudTextField T="string" Label="Regional Id" @bind-Value="Student.RegionalId" For="@(() => Student.RegionalId)"></MudTextField>
<MudTextField T="string" Label="State Id" @bind-Value="Student.StateId" For="@(() => Student.StateId)"></MudTextField>
<MudTextField T="string" Label="National Id" @bind-Value="Student.NationalId" For="@(() => Student.NationalId)"></MudTextField>
<MudSelect T="OfficerRole?" @bind-Value="@Student.OfficerRole" Label="Officer Role">
<MudSelectItem T="OfficerRole?" Value="@null">-not an officer-</MudSelectItem>
@foreach (var officerRole in Enum.GetValues(typeof(OfficerRole)).Cast<OfficerRole>())
{
<MudSelectItem T="OfficerRole?" Value="@(officerRole)"></MudSelectItem>
}
</MudSelect>
</MudPaper>
</MudItem>
</MudGrid>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="students">Back</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Save" OnClick="UpdateStudent">Save</MudButton>
</EditForm>
}
@code {
[SupplyParameterFromQuery]
private int Id { get; set; }
[SupplyParameterFromForm]
private Student? Student { get; set; }
protected override async Task OnInitializedAsync()
{
Student ??= await Context.Students.FirstOrDefaultAsync(m => m.Id == Id);
if (Student is null)
{
NavigationManager.NavigateTo("notfound");
}
}
// 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 UpdateStudent()
{
if (Student.OfficerRole == 0)
Student.OfficerRole = null;
Context.Attach(Student!).State = EntityState.Modified;
try
{
await Context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!StudentExists(Student!.Id))
{
NavigationManager.NavigateTo("notfound");
}
else
{
throw;
}
}
NavigationManager.NavigateTo("/students");
}
private bool StudentExists(int id)
{
return Context.Students.Any(e => e.Id == id);
}
}
@@ -1,157 +0,0 @@
@page "/students/event-ranking"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context
@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>
@if (!context.RankedEvents.Any(re => re.OnSiteActivity))
{
<MudTooltip Text="No On-Site Activity">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.OnSiteActivity</MudText>
</MudTooltip>
}
@if (!context.RankedEvents.Any(re => re.RegionalEvent))
{
<MudTooltip Text="No Regional Event">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.RegionalEvent</MudText>
</MudTooltip>
}
@if (context.RankedEvents.All(re => re.EventFormat != EventFormat.Individual))
{
<MudTooltip Text="No Individual Event">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.IndividualEvent</MudText>
</MudTooltip>
}
</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);
}
}
@@ -1,166 +0,0 @@
@page "/students/event-ranking-edit/{StudentId:int}"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using BlazorSortableList
@using WebApp.Models
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Student Event Ranks - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3"><MudIcon Icon="@AppIcons.EventRank"></MudIcon> Student Event Ranks</MudText>
<div>
@if (_student == null)
{
<p><em>Loading...</em></p>
}
else
{
<MudText Typo="Typo.h4">@_student.Name</MudText>
<MudText Color="Color.Warning">Warning: drag and drop is currently a bit squirrely - double check!</MudText>
<MudButton StartIcon="@Icons.Material.Filled.ArrowBack" Href="students/event-ranking">Back</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Save" OnClick="Save">Save</MudButton>
/* https://github.com/AlexNek/BlazorSortableList */
<MudGrid>
<MudItem xs="6" md="4" xl="3" Class="ranked-event-column">
<SortableList
Group="GroupId" Id="ListId1" Context="item"
Items="_rankedEvents" OnRemove="RankedEventsRemove" OnUpdate="Update">
<SortableItemTemplate>
<MudCard Outlined="true">
<MudCardContent>@item.Name</MudCardContent>
</MudCard>
</SortableItemTemplate>
</SortableList>
</MudItem>
<MudItem xs="6" md="4" xl="3">
<SortableList
Group="GroupId" Id="ListId2" Context="item"
Items="_availableEvents" OnRemove="AvailableEventsRemove" Sort="false">
<SortableItemTemplate>
<MudCard Outlined="true">
<MudCardContent>@item.Name</MudCardContent>
</MudCard>
</SortableItemTemplate>
</SortableList>
</MudItem>
</MudGrid>
}
</div>
@code {
private const string ListId1 = "SharedListId1";
private const string ListId2 = "SharedListId2";
private const string GroupId = "CommonGroup";
[Parameter] public int? StudentId { get; set; }
private Student? _student;
private List<EventDefinition>? _events;
public List<EventDefinition> _rankedEvents = [];
public List<EventDefinition> _availableEvents = [];
SharedSortableListGroup _group;
private void RankedEventsRemove((int oldIndex, int newIndex) indices)
{
// get the item at the old index in list 1
var item = _rankedEvents[indices.oldIndex];
// add it to the new index in list 2
_availableEvents.Insert(indices.newIndex, item);
// remove the item from the old index in list 1
_rankedEvents.Remove(_rankedEvents[indices.oldIndex]);
}
private void AvailableEventsRemove((int oldIndex, int newIndex) indices)
{
// get the item at the old index in list 2
var item = _availableEvents[indices.oldIndex];
// add it to the new index in list 1
_rankedEvents.Insert(indices.newIndex, item);
// remove the item from the old index in list 2
_availableEvents.Remove(_availableEvents[indices.oldIndex]);
}
protected override async Task OnInitializedAsync()
{
_student =
await Context.Students
.Include(e => e.EventRankings)
.Where(e => e.Id == StudentId).FirstAsync();
_events =
await Context.Events
.OrderBy(e => e.Name)
.ToListAsync();
_rankedEvents = _student.EventRankings.OrderBy(e => e.Rank).Select(e => e.EventDefinition).ToList();
_availableEvents = _events.Where(e => !_rankedEvents.Contains(e)).ToList();
_group = new SharedSortableListGroup(StateHasChanged);
_group.AddModel(ListId1, new SortableListModel<EventDefinition>(_rankedEvents) { Group = GroupId });
_group.AddModel(ListId2, new SortableListModel<EventDefinition>(_availableEvents) { Group = GroupId });
}
private void Update((int oldIndex, int newIndex) indices)
{
var (oldIndex, newIndex) = indices;
var items = _rankedEvents;
var itemToMove = items[oldIndex];
items.RemoveAt(oldIndex);
if (newIndex < items.Count)
{
items.Insert(newIndex, itemToMove);
}
else
{
items.Add(itemToMove);
}
StateHasChanged();
}
async Task Save()
{
if (_student == null)
return;
try
{
_student.EventRankings.Clear();
for (var index = 0; index < _rankedEvents.Count; index++)
{
var evt = _rankedEvents[index];
_student.EventRankings.Add(new StudentEventRanking
{
EventDefinition = evt,
Student = _student,
Rank = index + 1
});
}
Context.Students.Update(_student);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/students/event-ranking");
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
@@ -1,88 +0,0 @@
@page "/students"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<PageTitle>Students - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Students</MudText>
<MudButton StartIcon="@Icons.Material.Filled.Create" Href="students/create">Create New</MudButton>
<MudButton StartIcon="@AppIcons.EventRank" Href="students/event-ranking">Event Rankings</MudButton>
<MudDataGrid T="Student" ServerData="ServerReload" @ref="_dataGrid" Filterable="true" RowsPerPage="25">
<Columns>
@* <PropertyColumn Property="@(e => e.Name)" Title="First Name" SortBy="e => e.FirstName" /> *@
<TemplateColumn Title="Name" SortBy="e => e.LastName" Sortable="true" >
<CellTemplate>
@context.Item.LastNameFirstName
@if (context.Item.OfficerRole != null)
{
<MudChip T="string" Icon="@(AppIcons.OfficerRoleIcon(context.Item.OfficerRole.Value))">@context.Item.OfficerRole</MudChip>
}
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Grade (TSA Year)" SortBy="e => e.Grade" Sortable="true">
<CellTemplate>
@context.Item.Grade (@context.Item.TsaYear)
</CellTemplate>
</TemplateColumn>
<TemplateColumn>
<CellTemplate>
<CrudActions DetailsHref="@($"/students/details?id={context.Item!.Id}")"
EditHref="@($"/students/edit?id={context.Item!.Id}")"
DeleteOnClick="() => DeleteStudent(context.Item!)">
</CrudActions>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="Student"></MudDataGridPager>
</PagerContent>
</MudDataGrid>
@code {
MudDataGrid<Student> _dataGrid = null!;
private async Task<GridData<Student>> ServerReload(GridState<Student> state)
{
var query =
Context.Students.OrderBy(e => e.LastName)
.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<Student>
{
TotalItems = totalItems,
Items = pagedData
};
}
private async Task DeleteStudent(Student student)
{
//_isRowBlocked = true;
var result = await DialogService
.ShowMessageBox("Delete student",
(MarkupString)$"Are you sure want to delete <b>{student.Name}</b>? This cannot be undone.",
yesText:"Yes",
noText:"Cancel");
if (result == true)
{
Context.Students.Remove(student!);
await Context.SaveChangesAsync();
Snackbar.Add($"Delete event: Delete of Student {student.Name}", Severity.Info);
}
//_isRowBlocked = false;
StateHasChanged();
await _dataGrid.ReloadServerData();
}
}
@@ -1,464 +0,0 @@
@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");
}
}
}
@@ -1,153 +0,0 @@
@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");
}
}
@@ -1,115 +0,0 @@
@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);
}
}
@@ -1,123 +0,0 @@
@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();
}
}
@@ -1,108 +0,0 @@
@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();
}
}
@@ -1,236 +0,0 @@
@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();
}
}