Add Blazor WebApp and rework data handling to utilize Entity Framework
This commit is contained in:
@@ -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 × [@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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user