Add Blazor WebApp and rework data handling to utilize Entity Framework

This commit is contained in:
2025-09-11 11:49:48 -04:00
parent 5220e61c79
commit 3daa3b81b3
111 changed files with 6039 additions and 946 deletions
+14
View File
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace WebApp
{
public class ChapterSettings
{
public required string Name { get; set; }
public required string ShortName { get; set; }
public required string RegionalId { get; set; }
public required string StateId { get; set; }
public required string NationalId { get; set; }
public required string CompetitionYear { get; set; }
}
}
+24
View File
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="@Assets["_content/MudBlazor/MudBlazor.min.css"]" rel="stylesheet" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="WebApp.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script src="_framework/blazor.web.js"></script>
<script src="@Assets["_content/MudBlazor/MudBlazor.min.js"]"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
</body>
</html>
+27
View File
@@ -0,0 +1,27 @@
@inherits LayoutComponentBase
<MudThemeProvider />
<MudPopoverProvider />
<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<main>
@* <div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div> *@
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
+43
View File
@@ -0,0 +1,43 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">WebApp</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="events">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Events
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="students">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Students
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="teams">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Teams
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="import">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Import
</NavLink>
</div>
</nav>
</div>
+105
View File
@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}
+36
View File
@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}
@@ -0,0 +1,134 @@
@page "/events/create"
@using Microsoft.EntityFrameworkCore
@using Core.Entities
@using Data
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Create</PageTitle>
<h1>Create</h1>
<h2>EventDefinition</h2>
<hr />
<div class="row">
<div class="col-md-4">
<EditForm method="post" Model="EventDefinition" OnValidSubmit="AddEventDefinition" FormName="create" Enhance>
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert"/>
<div class="mb-3">
<label for="name" class="form-label">Name:</label>
<InputText id="name" @bind-Value="EventDefinition.Name" class="form-control" />
<ValidationMessage For="() => EventDefinition.Name" class="text-danger" />
</div>
<div class="mb-3">
<label for="shortname" class="form-label">ShortName:</label>
<InputText id="shortname" @bind-Value="EventDefinition.ShortName" class="form-control" />
<ValidationMessage For="() => EventDefinition.ShortName" class="text-danger" />
</div>
<div class="mb-3">
<label for="eventformat" class="form-label">EventFormat:</label>
<InputSelect @bind-Value="@EventDefinition.EventFormat">
@foreach (var format in Enum.GetValues(typeof(EventFormat)))
{
<option value="@format">@(@format.ToString())</option>
}
</InputSelect>
<ValidationMessage For="() => EventDefinition.EventFormat" class="text-danger" />
</div>
<div class="mb-3">
<label for="minteamsize" class="form-label">MinTeamSize:</label>
<InputNumber id="minteamsize" @bind-Value="EventDefinition.MinTeamSize" class="form-control" />
<ValidationMessage For="() => EventDefinition.MinTeamSize" class="text-danger" />
</div>
<div class="mb-3">
<label for="maxteamsize" class="form-label">MaxTeamSize:</label>
<InputNumber id="maxteamsize" @bind-Value="EventDefinition.MaxTeamSize" class="form-control" />
<ValidationMessage For="() => EventDefinition.MaxTeamSize" class="text-danger" />
</div>
<div class="mb-3">
<label for="semifinalistactivity" class="form-label">SemifinalistActivity:</label>
<InputText id="semifinalistactivity" @bind-Value="EventDefinition.SemifinalistActivity" class="form-control" />
<ValidationMessage For="() => EventDefinition.SemifinalistActivity" class="text-danger" />
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes:</label>
<InputText id="notes" @bind-Value="EventDefinition.Notes" class="form-control" />
<ValidationMessage For="() => EventDefinition.Notes" class="text-danger" />
</div>
<div class="mb-3">
<label for="maxteamcountstate" class="form-label">MaxTeamCountState:</label>
<InputNumber id="maxteamcountstate" @bind-Value="EventDefinition.MaxTeamCountState" class="form-control" />
<ValidationMessage For="() => EventDefinition.MaxTeamCountState" class="text-danger" />
</div>
<div class="mb-3">
<label for="regionalevent" class="form-label">RegionalEvent:</label>
<InputCheckbox id="regionalevent" @bind-Value="EventDefinition.RegionalEvent" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.RegionalEvent" class="text-danger" />
</div>
<div class="mb-3">
<label for="regionalpresubmit" class="form-label">RegionalPresubmit:</label>
<InputCheckbox id="regionalpresubmit" @bind-Value="EventDefinition.RegionalPresubmit" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.RegionalPresubmit" class="text-danger" />
</div>
<div class="mb-3">
<label for="statepresubmission" class="form-label">StatePresubmission:</label>
<InputCheckbox id="statepresubmission" @bind-Value="EventDefinition.StatePresubmission" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.StatePresubmission" class="text-danger" />
</div>
<div class="mb-3">
<label for="statepretesting" class="form-label">StatePretesting:</label>
<InputCheckbox id="statepretesting" @bind-Value="EventDefinition.StatePretesting" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.StatePretesting" class="text-danger" />
</div>
<div class="mb-3">
<label for="statepreliminaryround" class="form-label">StatePreliminaryRound:</label>
<InputCheckbox id="statepreliminaryround" @bind-Value="EventDefinition.StatePreliminaryRound" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.StatePreliminaryRound" class="text-danger" />
</div>
<div class="mb-3">
<label for="documentation" class="form-label">Documentation:</label>
<InputText id="documentation" @bind-Value="EventDefinition.Documentation" class="form-control" />
<ValidationMessage For="() => EventDefinition.Documentation" class="text-danger" />
</div>
<div class="mb-3">
<label for="eligibility" class="form-label">Eligibility:</label>
<InputText id="eligibility" @bind-Value="EventDefinition.Eligibility" class="form-control" />
<ValidationMessage For="() => EventDefinition.Eligibility" class="text-danger" />
</div>
<div class="mb-3">
<label for="theme" class="form-label">Theme:</label>
<InputText id="theme" @bind-Value="EventDefinition.Theme" class="form-control" />
<ValidationMessage For="() => EventDefinition.Theme" class="text-danger" />
</div>
<div class="mb-3">
<label for="description" class="form-label">Description:</label>
<InputText id="description" @bind-Value="EventDefinition.Description" class="form-control" />
<ValidationMessage For="() => EventDefinition.Description" class="text-danger" />
</div>
<div class="mb-3">
<label for="levelofeffort" class="form-label">LevelOfEffort:</label>
<InputNumber id="levelofeffort" @bind-Value="EventDefinition.LevelOfEffort" class="form-control" />
<ValidationMessage For="() => EventDefinition.LevelOfEffort" class="text-danger" />
</div>
<button type="submit" class="btn btn-primary">Create</button>
</EditForm>
</div>
</div>
<div>
<a href="/events">Back to List</a>
</div>
@code {
[SupplyParameterFromForm]
private EventDefinition EventDefinition { get; set; } = new();
// To protect from overposting attacks, see https://learn.microsoft.com/aspnet/core/blazor/forms/#mitigate-overposting-attacks.
private async Task AddEventDefinition()
{
context.Events.Add(EventDefinition);
await context.SaveChangesAsync();
NavigationManager.NavigateTo("/events");
}
}
@@ -0,0 +1,122 @@
@page "/events/delete"
@using Microsoft.EntityFrameworkCore
@using Core.Entities
@using Data
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Delete</PageTitle>
<h1>Delete</h1>
<p>Are you sure you want to delete this?</p>
<div>
<h2>EventDefinition</h2>
<hr />
@if (eventdefinition is null)
{
<p><em>Loading...</em></p>
}
else {
<dl class="row">
<dt class="col-sm-2">Name</dt>
<dd class="col-sm-10">@eventdefinition.Name</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Short Name</dt>
<dd class="col-sm-10">@eventdefinition.ShortName</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Event Format</dt>
<dd class="col-sm-10">@eventdefinition.EventFormat</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Min Team Size</dt>
<dd class="col-sm-10">@eventdefinition.MinTeamSize</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Max Team Size</dt>
<dd class="col-sm-10">@eventdefinition.MaxTeamSize</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">SemifinalistActivity</dt>
<dd class="col-sm-10">@eventdefinition.SemifinalistActivity</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Notes</dt>
<dd class="col-sm-10">@eventdefinition.Notes</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">MaxTeamCountState</dt>
<dd class="col-sm-10">@eventdefinition.MaxTeamCountState</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">RegionalEvent</dt>
<dd class="col-sm-10">@eventdefinition.RegionalEvent</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">RegionalPresubmit</dt>
<dd class="col-sm-10">@eventdefinition.RegionalPresubmit</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">StatePresubmission</dt>
<dd class="col-sm-10">@eventdefinition.StatePresubmission</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">StatePretesting</dt>
<dd class="col-sm-10">@eventdefinition.StatePretesting</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">StatePreliminaryRound</dt>
<dd class="col-sm-10">@eventdefinition.StatePreliminaryRound</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Documentation</dt>
<dd class="col-sm-10">@eventdefinition.Documentation</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Eligibility</dt>
<dd class="col-sm-10">@eventdefinition.Eligibility</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Theme</dt>
<dd class="col-sm-10">@eventdefinition.Theme</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Description</dt>
<dd class="col-sm-10">@eventdefinition.Description</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">LevelOfEffort</dt>
<dd class="col-sm-10">@eventdefinition.LevelOfEffort</dd>
</dl>
<EditForm method="post" Model="eventdefinition" OnValidSubmit="DeleteEventDefinition" FormName="delete" Enhance>
<button type="submit" class="btn btn-danger" disabled="@(eventdefinition is null)">Delete</button> |
<a href="/events">Back to List</a>
</EditForm>
}
</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");
}
}
private async Task DeleteEventDefinition()
{
context.Events.Remove(eventdefinition!);
await context.SaveChangesAsync();
NavigationManager.NavigateTo("/events");
}
}
@@ -0,0 +1,80 @@
@page "/events/details"
@using Microsoft.EntityFrameworkCore
@using Core.Entities
@using Data
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Details</PageTitle>
<h1>Details</h1>
<div>
<h2>EventDefinition</h2>
<hr />
@if (eventdefinition is null)
{
<p><em>Loading...</em></p>
}
else {
<dl class="row">
<dt class="col-sm-2">Name</dt>
<dd class="col-sm-10">@eventdefinition.Name</dd>
<dt class="col-sm-2">ShortName</dt>
<dd class="col-sm-10">@eventdefinition.ShortName</dd>
<dt class="col-sm-2">EventFormat</dt>
<dd class="col-sm-10">@eventdefinition.EventFormat</dd>
<dt class="col-sm-2">MinTeamSize</dt>
<dd class="col-sm-10">@eventdefinition.MinTeamSize</dd>
<dt class="col-sm-2">MaxTeamSize</dt>
<dd class="col-sm-10">@eventdefinition.MaxTeamSize</dd>
<dt class="col-sm-2">SemifinalistActivity</dt>
<dd class="col-sm-10">@eventdefinition.SemifinalistActivity</dd>
<dt class="col-sm-2">Notes</dt>
<dd class="col-sm-10">@eventdefinition.Notes</dd>
<dt class="col-sm-2">MaxTeamCountState</dt>
<dd class="col-sm-10">@eventdefinition.MaxTeamCountState</dd>
<dt class="col-sm-2">RegionalEvent</dt>
<dd class="col-sm-10">@eventdefinition.RegionalEvent</dd>
<dt class="col-sm-2">RegionalPresubmit</dt>
<dd class="col-sm-10">@eventdefinition.RegionalPresubmit</dd>
<dt class="col-sm-2">StatePresubmission</dt>
<dd class="col-sm-10">@eventdefinition.StatePresubmission</dd>
<dt class="col-sm-2">StatePretesting</dt>
<dd class="col-sm-10">@eventdefinition.StatePretesting</dd>
<dt class="col-sm-2">StatePreliminaryRound</dt>
<dd class="col-sm-10">@eventdefinition.StatePreliminaryRound</dd>
<dt class="col-sm-2">Documentation</dt>
<dd class="col-sm-10">@eventdefinition.Documentation</dd>
<dt class="col-sm-2">Eligibility</dt>
<dd class="col-sm-10">@eventdefinition.Eligibility</dd>
<dt class="col-sm-2">Theme</dt>
<dd class="col-sm-10">@eventdefinition.Theme</dd>
<dt class="col-sm-2">Description</dt>
<dd class="col-sm-10">@eventdefinition.Description</dd>
<dt class="col-sm-2">LevelOfEffort</dt>
<dd class="col-sm-10">@eventdefinition.LevelOfEffort</dd>
</dl>
<div>
<a href="@($"/eventdefinitions/edit?id={eventdefinition.Id}")">Edit</a> |
<a href="@($"/events")">Back to List</a>
</div>
}
</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");
}
}
}
@@ -0,0 +1,177 @@
@page "/events/edit"
@using Microsoft.EntityFrameworkCore
@using Core.Entities
@using Data
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Edit</PageTitle>
<h1>Edit</h1>
<h2>EventDefinition</h2>
<hr />
@if (EventDefinition is null)
{
<p><em>Loading...</em></p>
}
else
{
<div class="row">
<div class="col-md-4">
<EditForm method="post" Model="EventDefinition" OnValidSubmit="UpdateEventDefinition" FormName="edit" Enhance>
<DataAnnotationsValidator />
<ValidationSummary role="alert"/>
<input type="hidden" name="EventDefinition.Id" value="@EventDefinition.Id" />
<div class="mb-3">
<label for="name" class="form-label">Name:</label>
<InputText id="name" @bind-Value="EventDefinition.Name" class="form-control" />
<ValidationMessage For="() => EventDefinition.Name" class="text-danger" />
</div>
<div class="mb-3">
<label for="shortname" class="form-label">ShortName:</label>
<InputText id="shortname" @bind-Value="EventDefinition.ShortName" class="form-control" />
<ValidationMessage For="() => EventDefinition.ShortName" class="text-danger" />
</div>
<div class="mb-3">
<label for="eventformat" class="form-label">EventFormat:</label>
<InputSelect @bind-Value="@EventDefinition.EventFormat">
@foreach (var format in Enum.GetValues(typeof(EventFormat)))
{
<option value="@format">@(@format.ToString())</option>
}
</InputSelect>
<ValidationMessage For="() => EventDefinition.EventFormat" class="text-danger" />
</div>
<div class="mb-3">
<label for="minteamsize" class="form-label">MinTeamSize:</label>
<InputNumber id="minteamsize" @bind-Value="EventDefinition.MinTeamSize" class="form-control" />
<ValidationMessage For="() => EventDefinition.MinTeamSize" class="text-danger" />
</div>
<div class="mb-3">
<label for="maxteamsize" class="form-label">MaxTeamSize:</label>
<InputNumber id="maxteamsize" @bind-Value="EventDefinition.MaxTeamSize" class="form-control" />
<ValidationMessage For="() => EventDefinition.MaxTeamSize" class="text-danger" />
</div>
<div class="mb-3">
<label for="semifinalistactivity" class="form-label">SemifinalistActivity:</label>
<InputText id="semifinalistactivity" @bind-Value="EventDefinition.SemifinalistActivity" class="form-control" />
<ValidationMessage For="() => EventDefinition.SemifinalistActivity" class="text-danger" />
</div>
<div class="mb-3">
<label for="notes" class="form-label">Notes:</label>
<InputText id="notes" @bind-Value="EventDefinition.Notes" class="form-control" />
<ValidationMessage For="() => EventDefinition.Notes" class="text-danger" />
</div>
<div class="mb-3">
<label for="maxteamcountstate" class="form-label">MaxTeamCountState:</label>
<InputNumber id="maxteamcountstate" @bind-Value="EventDefinition.MaxTeamCountState" class="form-control" />
<ValidationMessage For="() => EventDefinition.MaxTeamCountState" class="text-danger" />
</div>
<div class="mb-3">
<label for="regionalevent" class="form-label">RegionalEvent:</label>
<InputCheckbox id="regionalevent" @bind-Value="EventDefinition.RegionalEvent" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.RegionalEvent" class="text-danger" />
</div>
<div class="mb-3">
<label for="regionalpresubmit" class="form-label">RegionalPresubmit:</label>
<InputCheckbox id="regionalpresubmit" @bind-Value="EventDefinition.RegionalPresubmit" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.RegionalPresubmit" class="text-danger" />
</div>
<div class="mb-3">
<label for="statepresubmission" class="form-label">StatePresubmission:</label>
<InputCheckbox id="statepresubmission" @bind-Value="EventDefinition.StatePresubmission" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.StatePresubmission" class="text-danger" />
</div>
<div class="mb-3">
<label for="statepretesting" class="form-label">StatePretesting:</label>
<InputCheckbox id="statepretesting" @bind-Value="EventDefinition.StatePretesting" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.StatePretesting" class="text-danger" />
</div>
<div class="mb-3">
<label for="statepreliminaryround" class="form-label">StatePreliminaryRound:</label>
<InputCheckbox id="statepreliminaryround" @bind-Value="EventDefinition.StatePreliminaryRound" class="form-check-input" />
<ValidationMessage For="() => EventDefinition.StatePreliminaryRound" class="text-danger" />
</div>
<div class="mb-3">
<label for="documentation" class="form-label">Documentation:</label>
<InputText id="documentation" @bind-Value="EventDefinition.Documentation" class="form-control" />
<ValidationMessage For="() => EventDefinition.Documentation" class="text-danger" />
</div>
<div class="mb-3">
<label for="eligibility" class="form-label">Eligibility:</label>
<InputText id="eligibility" @bind-Value="EventDefinition.Eligibility" class="form-control" />
<ValidationMessage For="() => EventDefinition.Eligibility" class="text-danger" />
</div>
<div class="mb-3">
<label for="theme" class="form-label">Theme:</label>
<InputText id="theme" @bind-Value="EventDefinition.Theme" class="form-control" />
<ValidationMessage For="() => EventDefinition.Theme" class="text-danger" />
</div>
<div class="mb-3">
<label for="description" class="form-label">Description:</label>
<InputText id="description" @bind-Value="EventDefinition.Description" class="form-control" />
<ValidationMessage For="() => EventDefinition.Description" class="text-danger" />
</div>
<div class="mb-3">
<label for="levelofeffort" class="form-label">LevelOfEffort:</label>
<InputNumber id="levelofeffort" @bind-Value="EventDefinition.LevelOfEffort" class="form-control" />
<ValidationMessage For="() => EventDefinition.LevelOfEffort" class="text-danger" />
</div>
<button type="submit" class="btn btn-primary">Save</button>
</EditForm>
</div>
</div>
}
<div>
<a href="/events">Back to List</a>
</div>
@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 async Task UpdateEventDefinition()
{
context.Attach(EventDefinition!).State = EntityState.Modified;
try
{
await 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);
}
}
@@ -0,0 +1,87 @@
@using Core.Entities
@using Data
@using Microsoft.EntityFrameworkCore
@page "/events/descriptions"
@inject IConfiguration Configuration
@inject AppDbContext Context
@rendermode InteractiveServer
<PageTitle>TSA Events @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<h1>TSA Events @Configuration["ChapterSettings:CompetitionYear"]</h1>
@if (_events == null)
{
<p><em>Loading...</em></p>
}
else
{
<div>
@foreach (var evt in _events)
{
<div class="container nobrk">
@if (evt.RegionalEvent)
{
<div class="row">
<div class="col">
<i>Regional Event</i>
</div>
</div>
}
<div div class="row">
<div class="col-4">
<h5>@evt.Name</h5>
</div>
<div class="col-2">
@if (evt.EventFormat is EventFormat.Team)
{
<html><strong>@evt.EventFormat</strong><br/>Size: <strong>@evt.TeamSize</strong></html>
}
else
{
<html>
<strong>@evt.EventFormat</strong>
</html>
}
</div>
<div class="col">
Eligibility: @evt.Eligibility
</div>
<div class="col-1">
<strong> Effort</strong>: @evt.LevelOfEffort
</div>
<div class="col-2">
<strong>Activity</strong>: @evt.SemifinalistActivity
</div>
</div>
<div div class="row mt-3">
<div class="col">@evt.Description</div></div>
@if (!string.IsNullOrEmpty(evt.Theme))
{
<div div class="row mt-2">
<div class="col-3 text-center"><i>Theme for 2025-26:</i></div>
<div class="col" style="white-space:pre-wrap;">@evt.Theme</div>
</div>
}
@if (!string.IsNullOrEmpty(evt.Documentation))
{
<div div class="row mt-2">
<div class="col-3 text-center"><i>Materials:</i></div>
<div class="col">@evt.Documentation</div>
</div>
}
<hr/>
</div>
}
</div>
}
@code {
private EventDefinition[]? _events = null;
protected override async Task OnInitializedAsync()
{
_events = await Context.Events.OrderBy(e => e.Name).Where(e => e.Name != "Chapter Team").ToArrayAsync();
}
}
@@ -0,0 +1,91 @@
@page "/events"
@using Microsoft.EntityFrameworkCore
@inject AppDbContext Context
<PageTitle>Events - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Events</MudText>
<MudButton StartIcon="@Icons.Material.Filled.Create" Href="events/create">Create New</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" />
<PropertyColumn Property="@(e => e.LevelOfEffort)" Title="Level of Effort" />
<PropertyColumn Property="@(e => e.SemifinalistActivity)" Title="On-site Activity" />
<PropertyColumn Property="@(e => e.RegionalEvent)" Title="Regional Event" />
<TemplateColumn Title="Team Size" CellStyle="white-space:nowrap">
<CellTemplate>
[@context.Item.MinTeamSize&nbsp;-&nbsp;@context.Item.MaxTeamSize]
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Teams State #">
<CellTemplate>
@context.Item.MaxTeamCountState
</CellTemplate>
</TemplateColumn>
<TemplateColumn>
<CellTemplate>
<MudStack Row>
<MudButtonGroup Size="Size.Small">
<MudTooltip Text="Details">
<MudIconButton Href="@($"/events/details?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Description">Details</MudIconButton>
</MudTooltip>
<MudTooltip Text="Edit">
<MudIconButton Href="@($"/events/edit?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Edit">Edit</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete">
<MudIconButton Href="@($"/events/delete?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Delete" Color="@Color.Warning">Delete</MudIconButton>
</MudTooltip>
</MudButtonGroup>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="EventDefinition"></MudDataGridPager>
</PagerContent>
</MudDataGrid>
@*
<QuickGrid Class="table" Items="context.Events">
<PropertyColumn Property="eventdefinition => eventdefinition.Name" />
<PropertyColumn Property="eventdefinition => eventdefinition.EventFormat" />
@* <PropertyColumn Property="eventdefinition => eventdefinition.MinTeamSize" />
<PropertyColumn Property="eventdefinition => eventdefinition.MaxTeamSize" />
<PropertyColumn Property="eventdefinition => eventdefinition.SemifinalistActivity" />
<PropertyColumn Property="eventdefinition => eventdefinition.Notes" />
<PropertyColumn Property="eventdefinition => eventdefinition.MaxTeamCountState" />
<PropertyColumn Property="eventdefinition => eventdefinition.RegionalEvent" />
<PropertyColumn Property="eventdefinition => eventdefinition.RegionalPresubmit" />
<PropertyColumn Property="eventdefinition => eventdefinition.StatePresubmission" />
<PropertyColumn Property="eventdefinition => eventdefinition.StatePretesting" />
<PropertyColumn Property="eventdefinition => eventdefinition.StatePreliminaryRound" />
<PropertyColumn Property="eventdefinition => eventdefinition.Documentation" />
<PropertyColumn Property="eventdefinition => eventdefinition.Eligibility" />
<PropertyColumn Property="eventdefinition => eventdefinition.Theme" />
<PropertyColumn Property="eventdefinition => eventdefinition.Description" />
<PropertyColumn Property="eventdefinition => eventdefinition.LevelOfEffort" />
*@
@code {
MudDataGrid<EventDefinition> _dataGrid = null!;
private async Task<GridData<EventDefinition>> ServerReload(GridState<EventDefinition> state)
{
var query = Context.Events.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
};
}
}
+13
View File
@@ -0,0 +1,13 @@
@page "/"
@inject IConfiguration Configuration
<PageTitle>Home</PageTitle>
<MudText Typo="Typo.h2">TSA Chapter Organizer</MudText>
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Name"]</MudText>
<MudLink Href="events">
<MudCard>
<MudCardHeader>Events</MudCardHeader>
</MudCard>
</MudLink>
+113
View File
@@ -0,0 +1,113 @@
@page "/import"
@using Core.Parsers
@using Microsoft.EntityFrameworkCore
@inject AppDbContext Context
@rendermode InteractiveServer
<PageTitle>Import Data</PageTitle>
<h1>Import Data</h1>
<h3>Events</h3>
<InputFile OnChange="UploadEvents"></InputFile>
<text>@_events?.Length Events</text>
<button class="btn btn-primary" @onclick="SaveEvents">Save to Database</button>
<br/>
<h3>Students</h3>
<InputFile OnChange="UploadStudents"></InputFile>
<text>@_students?.Length Students</text>
<button class="btn btn-primary" @onclick="SaveStudents">Save to Database</button>
@code {
private EventDefinition[]? _events;
private Student[]? _students;
async Task UploadEvents(InputFileChangeEventArgs arg)
{
await GetStreamReaderFromInputFile(arg, reader =>
{
var eventDefinitionParser = new EventDefinitionParser(reader);
_events = eventDefinitionParser.Parse();
});
}
async Task SaveEvents()
{
if (_events == null)
return;
foreach (var evt in _events)
{
// check if it already exists
var exists
= await Context.Events
.FirstOrDefaultAsync(e => e.Name == evt.Name);
if (exists != null)
continue;
await Context.Events.AddAsync(evt);
}
await Context.SaveChangesAsync();
}
async Task UploadStudents(InputFileChangeEventArgs arg)
{
await GetStreamReaderFromInputFile(arg, reader =>
{
var studentParser = new StudentParser(reader);
_students = studentParser.Parse();
});
}
async Task SaveStudents()
{
if (_students == null)
return;
try
{
foreach (var student in _students)
{
// check if it already exists
var exists
= await Context.Students
.FirstOrDefaultAsync(e
=> e.FirstName == student.FirstName
&& e.LastName == student.LastName);
if (exists != null)
continue;
await Context.Students.AddAsync(student);
}
await Context.SaveChangesAsync();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
static async Task GetStreamReaderFromInputFile(InputFileChangeEventArgs arg, Action<StreamReader> f)
{
StreamReader? streamReader = null;
try
{
var browserFile = arg.File;
await using var fs = browserFile.OpenReadStream();
await using var ms = new MemoryStream();
await fs.CopyToAsync(ms);
ms.Seek(0,0);
streamReader = new StreamReader(ms);
f(streamReader);
}
catch
{
streamReader?.Dispose();
throw;
}
}
}
@@ -0,0 +1,57 @@
@page "/students/create"
@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 />
<div class="row">
<div class="col-md-4">
<EditForm method="post" Model="Student" OnValidSubmit="AddStudent" FormName="create" Enhance>
<DataAnnotationsValidator />
<ValidationSummary class="text-danger" role="alert"/>
<div class="mb-3">
<label for="firstname" class="form-label">First Name:</label>
<InputText id="firstname" @bind-Value="Student.FirstName" class="form-control" />
<ValidationMessage For="() => Student.FirstName" class="text-danger" />
</div>
<div class="mb-3">
<label for="lastname" class="form-label">Last Name:</label>
<InputText id="lastname" @bind-Value="Student.LastName" class="form-control" />
<ValidationMessage For="() => Student.LastName" class="text-danger" />
</div>
<div class="mb-3">
<label for="grade" class="form-label">Grade:</label>
<InputNumber id="grade" @bind-Value="Student.Grade" class="form-control" />
<ValidationMessage For="() => Student.Grade" class="text-danger" />
</div>
<div class="mb-3">
<label for="tsayear" class="form-label">TSA Year:</label>
<InputNumber id="tsayear" @bind-Value="Student.TsaYear" class="form-control" />
<ValidationMessage For="() => Student.TsaYear" class="text-danger" />
</div>
<button type="submit" class="btn btn-primary">Create</button>
</EditForm>
</div>
</div>
<div>
<a href="/students">Back to List</a>
</div>
@code {
[SupplyParameterFromForm]
private Student Student { get; set; } = new() { TsaYear = 1 };
private async Task AddStudent()
{
Context.Students.Add(Student);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/students");
}
}
@@ -0,0 +1,80 @@
@page "/students/delete"
@using Microsoft.EntityFrameworkCore
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Delete Student - TSA Chapter Organizer</PageTitle>
<h1>Delete</h1>
<p>Are you sure you want to delete this?</p>
<div>
<h2>Student</h2>
<hr />
@if (student is null)
{
<p><em>Loading...</em></p>
}
else {
<dl class="row">
<dt class="col-sm-2">FirstName</dt>
<dd class="col-sm-10">@student.FirstName</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">LastName</dt>
<dd class="col-sm-10">@student.LastName</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">Grade</dt>
<dd class="col-sm-10">@student.Grade</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">StateId</dt>
<dd class="col-sm-10">@student.StateId</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">RegionalId</dt>
<dd class="col-sm-10">@student.RegionalId</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">NationalId</dt>
<dd class="col-sm-10">@student.NationalId</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">TsaYear</dt>
<dd class="col-sm-10">@student.TsaYear</dd>
</dl>
<dl class="row">
<dt class="col-sm-2">OfficerRole</dt>
<dd class="col-sm-10">@student.OfficerRole</dd>
</dl>
<EditForm method="post" Model="student" OnValidSubmit="DeleteStudent" FormName="delete" Enhance>
<button type="submit" class="btn btn-danger" disabled="@(student is null)">Delete</button> |
<a href="/students">Back to List</a>
</EditForm>
}
</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");
}
}
private async Task DeleteStudent()
{
context.Students.Remove(student!);
await context.SaveChangesAsync();
NavigationManager.NavigateTo("/students");
}
}
@@ -0,0 +1,64 @@
@page "/students/details"
@using Microsoft.EntityFrameworkCore
@using Core.Entities
@using Data
@inject AppDbContext context
@inject NavigationManager NavigationManager
<PageTitle>Student Details - TSA Chapter Organizer</PageTitle>
<h1>Details</h1>
<div>
<h2>Student</h2>
<hr />
@if (student is null)
{
<p><em>Loading...</em></p>
}
else {
<dl class="row">
<dt class="col-sm-2">FirstName</dt>
<dd class="col-sm-10">@student.FirstName</dd>
<dt class="col-sm-2">LastName</dt>
<dd class="col-sm-10">@student.LastName</dd>
<dt class="col-sm-2">Grade</dt>
<dd class="col-sm-10">@student.Grade</dd>
<dt class="col-sm-2">Email</dt>
<dd class="col-sm-10">@student.Email</dd>
<dt class="col-sm-2">PhoneNumber</dt>
<dd class="col-sm-10">@student.PhoneNumber</dd>
<dt class="col-sm-2">TsaYear</dt>
<dd class="col-sm-10">@student.TsaYear</dd>
<dt class="col-sm-2">StateId</dt>
<dd class="col-sm-10">@student.StateId</dd>
<dt class="col-sm-2">RegionalId</dt>
<dd class="col-sm-10">@student.RegionalId</dd>
<dt class="col-sm-2">NationalId</dt>
<dd class="col-sm-10">@student.NationalId</dd>
<dt class="col-sm-2">OfficerRole</dt>
<dd class="col-sm-10">@student.OfficerRole</dd>
</dl>
<div>
<a href="@($"/students/edit?id={student.Id}")">Edit</a> |
<a href="@($"/students")">Back to List</a>
</div>
}
</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");
}
}
}
@@ -0,0 +1,100 @@
@page "/students/edit"
@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>
<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);
}
}
@@ -0,0 +1,154 @@
@using Microsoft.EntityFrameworkCore
@page "/students/event-ranking"
@inject AppDbContext Context
@rendermode InteractiveServer
<PageTitle>Student Event Ranks - TSA Chapter Organizer</PageTitle>
@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>1st</MudTh>
<MudTh>2nd</MudTh>
<MudTh>3rd</MudTh>
<MudTh>4th</MudTh>
<MudTh>5th</MudTh>
<MudTh>6th</MudTh>
<MudTh></MudTh>
<MudTh>Warnings</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.FirstName</MudTd>
<MudTh>@context.Grade</MudTh>
<MudTh>@context.TsaYear</MudTh>
@for (var i = 1; i <= 6; i++)
{
var st = context.EventRankings.FirstOrDefault(e => e.Rank == i);
<MudTd Class="@($"event-rank-{i})")">
@if (st != null)
{
<span>@st.EventDefinition.ShortName&nbsp;
@if(st.EventDefinition.EventFormat == EventFormat.Individual) { <span>ⓘ</span>}
@if(st.EventDefinition.RegionalEvent) { <span>ⓡ</span>}
@if(st.EventDefinition.OnSiteActivity) { <span>ⓐ</span>}
</span>
}
</MudTd>
}
<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">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.LocalActivity"></MudIcon>
</MudTooltip>
}
@if (!context.RankedEvents.Any(re => re.RegionalEvent))
{
<MudTooltip Text="No Regional Event">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.PinDrop"></MudIcon>
</MudTooltip>
}
@if (context.RankedEvents.All(re => re.EventFormat != EventFormat.Individual))
{
<MudTooltip Text="No Individual Event">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.Person"></MudIcon>
</MudTooltip>
}
</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);
}
}
@@ -0,0 +1,165 @@
@using Microsoft.EntityFrameworkCore
@using BlazorSortableList
@using WebApp.Models
@page "/students/event-ranking-edit/{StudentId:int}"
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Student Event Ranks - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">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;
}
}
}
@@ -0,0 +1,70 @@
@page "/students"
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context
<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="@Icons.Material.Filled.AddChart" 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.FirstName" Sortable="true">
<CellTemplate>
@context.Item.Name
@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>
<MudStack Row>
<MudButtonGroup Size="Size.Small">
<MudTooltip Text="Details">
<MudIconButton Href="@($"/students/details?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Description">Details</MudIconButton>
</MudTooltip>
<MudTooltip Text="Edit">
<MudIconButton Href="@($"/students/edit?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Edit">Edit</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete">
<MudIconButton Href="@($"/students/delete?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Delete" Color="@Color.Warning">Delete</MudIconButton>
</MudTooltip>
</MudButtonGroup>
</MudStack>
</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.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
};
}
}
@@ -0,0 +1,365 @@
@page "/teams/assignment"
@using Core.Calculation
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using EventAssignment = Core.Calculation.EventAssignment
@inject AppDbContext Context
@inject NavigationManager NavigationManager
<PageTitle>Event Assignment - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Assignment</MudText>
<MudText>Optimized team assignments based on the student event rankings</MudText>
<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">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.LocalActivity"></MudIcon>
</MudTooltip>
}
@if (!context.HasRegionalEvent)
{
<MudTooltip Text="No Regional Event">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.PinDrop"></MudIcon>
</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.RankedEvent(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
@{
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 Title="@($"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 Title="@($"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 Title="@($"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 Title="@($"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 Teams [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></MudTd>
<MudTd Style="white-space:nowrap">@thresholds.StudentRankingCount, @thresholds.TeamCount &times; [@thresholds.LowerBound-@thresholds.UpperBound]</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTr>
<td colspan="2">
@foreach (var student in
context.Students
.OrderBy(e =>
e.EventRankings
.Find(er => er.EventDefinition == context.Event)?.Rank ?? 10)
.ThenBy(s => s.Grade + s.TsaYear))
{
var eventRank =
student.EventRankings
.Find(e => e.EventDefinition == context.Event)?.Rank;
var color = AppIcons.RankedEvent(eventRank ?? 0);
<MudPaper Class="d-inline-flex pa-2 mx-3 my-1" Style="@($"background:{color};")">
@student.FirstName
</MudPaper>
}
<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>
<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">
<HeaderContent>
<MudTh></MudTh>
<MudTh>Student</MudTh>
<MudTh>Event</MudTh>
</HeaderContent>
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle"
OnClick="() => RemoveRequireEvent(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.Student.FirstName</MudTd>
<MudTd Class="align-center">
@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>
</MudGrid>
<MudButton OnClick="Solve" Variant="Variant.Filled" Disabled="@_isSolving">Solve</MudButton>
</MudPaper>
<MudButton StartIcon="@Icons.Material.Filled.Edit" Href="students/event-ranking">Edit Student Event Rankings</MudButton>
@code {
public bool TestSwitch { get; set; } = false;
private readonly AssignmentParameters _parameters = new () {LimitTeamsToOne = false};
private List<EventDefinition>? _events;
private List<Student>? _students;
private List<EventAssignmentThresholds> _eventAssignmentThresholds = [];
MudTable<Team> _teamData;
MudTable<StudentEventStatistics> _statisticData;
private List<StudentEventStatistics> _statistics = [];
MudTable<AssignmentRequirement> _assignmentRequirementData;
private List<AssignmentRequirement> _assignmentRequirements = [];
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.Teams.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);
}
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
return new TableData<Team> { Items = solution.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 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();
}
}
@@ -0,0 +1,53 @@
@page "/teams/create"
@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>
<DataAnnotationsValidator />
<MudGrid>
<MudItem xs="12" sm="7">
<MudPaper Class="pa-4">
<MudSelect T="EventDefinition" @bind-Value="@Team.Event" Label="Event">
@foreach (var evt in _events)
{
<MudSelectItem T="EventDefinition" Value="@(evt)"></MudSelectItem>
}
</MudSelect>
<MudTextField T="string" Label="Name" @bind-Value="Team.Name" For="@(() => Team.Name)"></MudTextField>
</MudPaper>
</MudItem>
</MudGrid>
<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;
protected override async Task OnInitializedAsync()
{
_events =
await Context.Events
.OrderBy(e => e.Name)
.ToListAsync();
}
private async Task AddTeam()
{
Context.Teams.Add(Team);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/teams");
}
}
@@ -0,0 +1,75 @@
@page "/teams"
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context
<PageTitle>Teams</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>
<MudDataGrid T="Team" ServerData="ServerReload" @ref="_dataGrid" Filterable="true" RowsPerPage="25">
<Columns>
<PropertyColumn Property="@(e => e.Name)" Title="Name" />
<PropertyColumn Property="@(e => e.Event.Name)" Title="Event" />
@* <TemplateColumn Title="Name" SortBy="e => e.FirstName" Sortable="true">
<CellTemplate>
@context.Item.Name
@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>
<MudStack Row>
<MudButtonGroup Size="Size.Small">
<MudTooltip Text="Details">
<MudIconButton Href="@($"/teams/details?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Description">Details</MudIconButton>
</MudTooltip>
<MudTooltip Text="Edit">
<MudIconButton Href="@($"/teams/edit?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Edit">Edit</MudIconButton>
</MudTooltip>
<MudTooltip Text="Delete">
<MudIconButton Href="@($"/teams/delete?id={context.Item.Id}")" Icon="@Icons.Material.Filled.Delete" Color="@Color.Warning">Delete</MudIconButton>
</MudTooltip>
</MudButtonGroup>
</MudStack>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="Student"></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)
.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
};
}
}
+6
View File
@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
+13
View File
@@ -0,0 +1,13 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using WebApp
@using WebApp.Components
@using MudBlazor
@using Core.Entities
@using Data
+57
View File
@@ -0,0 +1,57 @@
using Core.Entities;
using MudBlazor;
namespace WebApp.Models
{
public static class AppIcons
{
private const string Prefix = "@Icons.Material.Filled.";
public static string LevelOfEffortIcon(int loe)
{
return loe switch
{
1 => MudBlazor.Icons.Material.Filled.StarBorder,
2 => MudBlazor.Icons.Material.Filled.StarHalf,
3 => MudBlazor.Icons.Material.Filled.Star,
_ => MudBlazor.Icons.Material.Filled.QuestionMark
};
}
public static string OfficerRoleIcon(OfficerRole officerRole)
{
return officerRole switch
{
OfficerRole.President => MudBlazor.Icons.Material.Filled.Gavel,
OfficerRole.VicePresident => MudBlazor.Icons.Material.Filled.StarBorderPurple500,
OfficerRole.Secretary => MudBlazor.Icons.Material.Filled.Draw,
OfficerRole.Treasurer => MudBlazor.Icons.Material.Filled.Key,
OfficerRole.Reporter => MudBlazor.Icons.Material.Filled.Mic,
OfficerRole.SergeantAtArms => MudBlazor.Icons.Material.Filled.Handshake,
_ => throw new ArgumentOutOfRangeException(nameof(officerRole), officerRole, null)
};
}
public static string RankedEvent(int rank)
{
return rank switch
{
1 => "#dd7e6b",
2 => "#ea9999",
3 => "#f9cb9c",
4 => "#ffe599",
5 => "#fff2cc",
6 => "#fffaea",
_ => "#ddd"
};
}
/*
* #dd7e6b;
#ea9999;
#f9cb9c;
#ffe599;
#fff2cc;
#fffaea;
*/
}
}
+16
View File
@@ -0,0 +1,16 @@
using BlazorSortableList;
using Core.Entities;
namespace WebApp.Models;
/// <summary>
/// Class SharedSortableListGroup.
/// Used for BlazorSortableList
/// </summary>
internal class SharedSortableListGroup : MultiSortableListGroup<EventDefinition>
{
public SharedSortableListGroup(Action refreshComponent)
: base(refreshComponent)
{
}
}
+41
View File
@@ -0,0 +1,41 @@
using Data;
using Microsoft.EntityFrameworkCore;
using MudBlazor.Services;
using WebApp.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddMudServices();
// Configure SQLite
var connectionString = builder.Configuration.GetConnectionString("SQLiteDefault");
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite(connectionString));
builder.Services.AddQuickGridEntityFrameworkAdapter();
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
app.UseMigrationsEndPoint();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
+58
View File
@@ -0,0 +1,58 @@
{
"profiles": {
"http": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5013"
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7235;http://localhost:5013"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Rank": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "students/event-ranking-edit/15",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7235;http://localhost:5013"
},
"Assignment": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "teams/assignment",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7235;http://localhost:5013"
}
},
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:31382",
"sslPort": 44365
}
}
}
+34
View File
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BlazorSortableList" Version="2.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid.EntityFrameworkAdapter" Version="9.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="MudBlazor" Version="8.12.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
<ProjectReference Include="..\Data\Data.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Components\Temp\" />
</ItemGroup>
</Project>
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"SQLiteDefault": "Data Source=ChapterOrganizer.db"
},
"Name" : "Test" ,
"ChapterSettings": {
"Name": "Robertsville Middle School",
"ShortName": "RMS",
"NationalId": "2227",
"StateId": "12227",
"RegionalId": "12227",
"CompetitionYear": "2026"
}
}
+91
View File
@@ -0,0 +1,91 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}
@media print {
/* body .container {
max-width: 1200px;
}*/
main {
font-size: 11px;
margin: 30pt;
color: #000;
background-color: #fff;
}
body .sidebar, main > div.top-row {
display: none;
}
.nobrk {
break-inside: avoid;
}
}
.ranked-event-column > div:only-child{
height:100%;
}
.ranked-event-column > div > div:nth-child(1) > .mud-card-content { background-color: #dd7e6b; }
.ranked-event-column > div > div:nth-child(2) > .mud-card-content { background-color: #ea9999; }
.ranked-event-column > div > div:nth-child(3) > .mud-card-content { background-color: #f9cb9c; }
.ranked-event-column > div > div:nth-child(4) > .mud-card-content { background-color: #ffe599; }
.ranked-event-column > div > div:nth-child(5) > .mud-card-content { background-color: #fff2cc; }
.ranked-event-column > div > div:nth-child(6) > .mud-card-content { background-color: #fffaea; }
.event-rank-1 { background-color: #dd7e6b; }
.event-rank-2 { background-color: #ea9999; }
.event-rank-3 { background-color: #f9cb9c; }
.event-rank-4 { background-color: #ffe599; }
.event-rank-5 { background-color: #fff2cc; }
.event-rank-6 { background-color: #fffaea; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB