Files
chapter-organizer/WebApp/Components/Features/Teams/Assignment.razor
T

477 lines
21 KiB
Plaintext

@page "/teams/assignment"
@attribute [Authorize]
@using Core.Calculation
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using EventAssignment = Core.Calculation.EventAssignment
@inject AppDbContext Context
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<PageTitle>Event Assignment - TSA Chapter Organizer</PageTitle>
<MudText Typo="Typo.h3">Assignment</MudText>
<MudGrid>
<MudItem><MudText>Optimized team assignments based on the student event rankings</MudText></MudItem>
<MudItem><MudButton StartIcon="@Icons.Material.Filled.Edit" Href="students/event-ranking">Edit Student Event Rankings</MudButton></MudItem>
</MudGrid>
<MudPaper Class="pa-4 mt-5">
<MudGrid>
<MudItem Style="width:160px;">
<MudNumericField @bind-Value="_parameters.TeamSizeLimit"
Label="Team Size Limit" Min="3" Max="8"></MudNumericField>
</MudItem>
<MudItem>
<MudTooltip Text="Require at least one On-Site Event">
<MudSwitch @bind-Value="_parameters.RequireOnSite" Color="Color.Info"
Label="On-Site" />
</MudTooltip>
</MudItem>
<MudItem>
<MudTooltip Text="Require at least one Regional Event">
<MudSwitch @bind-Value="_parameters.RequireRegional" Color="Color.Info"
Label="Regional" />
</MudTooltip>
</MudItem>
<MudItem>
<MudStack Style="width:100px;">
<MudTooltip Text="Student Event Count Assignment Range">
<MudInputLabel>Event Count</MudInputLabel>
</MudTooltip>
<MudNumericField @bind-Value="_parameters.EventsLowerBound"
Label="At Least" Min="2" Max="4"></MudNumericField>
<MudNumericField @bind-Value="_parameters.EventsUpperBound"
Label="Up to" Min="3" Max="5"></MudNumericField>
</MudStack>
</MudItem>
<MudItem>
<MudStack Style="width:100px;">
<MudTooltip Text="Student Level of Effort Range">
<MudInputLabel>LOE</MudInputLabel>
</MudTooltip>
<MudNumericField @bind-Value="_parameters.EffortLowerBound"
Label="At Least" Min="4" Max="7"></MudNumericField>
<MudNumericField @bind-Value="_parameters.EffortUpperBound"
Label="Up to" Min="7" Max="12"></MudNumericField>
</MudStack>
</MudItem>
<MudItem>
<MudInputLabel>Assignment Requirements</MudInputLabel>
<MudTable T="AssignmentRequirement" ServerData="ReloadAssignmentRequirements" @ref="_assignmentRequirementData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveRequireEvent(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">
@item.Student.FirstName
@item.EventDefinition.ShortName
@if (item.Requirement == Requirement.Include)
{
<MudIcon Class="ml-3" Icon="@Icons.Material.Filled.ThumbUp" Size="Size.Small"></MudIcon>
}
@if (item.Requirement == Requirement.Exclude)
{
<MudIcon Class="ml-3" Icon="@Icons.Material.Filled.ThumbDownAlt" Size="Size.Small"></MudIcon>
}
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem>
<MudInputLabel>Two Team Events</MudInputLabel>
<MudTable T="EventDefinition" ServerData="ReloadEventTwoTeam" @ref="_eventTwoTeamData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveTwoTeam(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.ShortName</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem>
<MudInputLabel>Omitted Events</MudInputLabel>
<MudTable T="EventDefinition" ServerData="ReloadOmittedEvents" @ref="_eventOmittedData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveOmitted(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.ShortName</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
</MudGrid>
<MudButton Class="ma-3" OnClick="Solve" Variant="Variant.Filled" Color="Color.Primary" Disabled="@_isSolving">Solve</MudButton>
</MudPaper>
<MudGrid>
<MudItem xs="12" lg="8">
<MudText Typo="Typo.h4">Students</MudText>
<MudTable T="StudentEventStatistics" ServerData="ReloadStatistics" @ref="_statisticData" >
<ColGroup>
<col style="width: 150px;" />
<col style="width: 50px;" />
<col style="width: 50px;" />
<col style="width: 60px;" />
</ColGroup>
<HeaderContent>
<MudTh>Student</MudTh>
<MudTh><MudTooltip Text="How many events they're assigned'">Event Count</MudTooltip></MudTh>
<MudTh><MudTooltip Text="Level of Effort Total">LOE Sum</MudTooltip></MudTh>
<MudTh><MudTooltip Text="Assignment Warnings">Warnings</MudTooltip></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><b>@context.Student.FirstName</b></MudTd>
<MudTd>@context.EventCount</MudTd>
<MudTd>@context.TotalLevelOfEffort</MudTd>
<MudTd>@if (!context.HasOnSiteActivity)
{
<MudTooltip Text="No On-Site Activity">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.OnSiteActivity</MudText>
</MudTooltip>
}
@if (!context.HasRegionalEvent)
{
<MudTooltip Text="No Regional Event">
<MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.RegionalEvent</MudText>
</MudTooltip>
}
</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTr><td colspan="4">
@{
var allStudentEvents =
context.Student.EventRankings
.OrderBy(e => e.Rank)
.Select(e => e.EventDefinition)
.Concat(context.Events)
.Distinct();
}
@foreach (var e in
allStudentEvents
.OrderBy(e =>
context.Student.EventRankings
.Find(ser => ser.EventDefinition == e)?.Rank ?? 10))
{
var eventRank = context.Student.EventRankings.Find(er => er.EventDefinition == e)?.Rank;
var isAssigned = context.Events.Contains(e);
var color = AppIcons.RankedEventColor(eventRank ?? 0);
var style = string.Empty;
if (isAssigned)
{
style += "border-color:black; border-width:thin;";
if (eventRank.HasValue)
{
style += $"background:{color};";
if (eventRank == 1)
style += $"color:black";
}
else
style += $"background:{Colors.Gray.Lighten3};";
}
else
{
if (eventRank.HasValue)
style += $"border-color:{color}; border-width:medium; color:{Colors.Gray.Lighten1};";
}
<MudPaper Class="d-inline-flex align-center pa-2 mx-3 my-1 border-solid" Style="@(style)">
@e.ShortName&nbsp;
@AppIcons.EventAttributes(e)
@{
var isIncluded = _assignmentRequirements
.Find(ar =>
ar.EventDefinition == e
&& ar.Student == context.Student
&& ar.Requirement == Requirement.Include) == null;
var isExcluded = _assignmentRequirements
.Find(ar =>
ar.EventDefinition == e
&& ar.Student == context.Student
&& ar.Requirement == Requirement.Exclude) == null;
}
@if (isIncluded)
{
<MudTooltip Text="@($"Add requirement for {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Outlined.ThumbUpAlt" Class="ml-3" Size="Size.Small" Color="Color.Default"
OnClick="() => RequireEvent(e, context.Student, Requirement.Include)"></MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="@($"Remove requirement for {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Filled.ThumbUpAlt" Class="ml-3" Size="Size.Small" Color="Color.Dark"
OnClick="() => RemoveRequireEvent(e, context.Student, Requirement.Include)"></MudIconButton>
</MudTooltip>
}
@if (isExcluded)
{
<MudTooltip Text="@($"Add restriction against {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Outlined.ThumbDownAlt" Size="Size.Small" Color="Color.Default"
OnClick="() => RequireEvent(e, context.Student, Requirement.Exclude)"></MudIconButton>
</MudTooltip>
}
else
{
<MudTooltip Text="@($"Remove restriction against {context.Student.FirstName} in {e.ShortName}")">
<MudIconButton Icon="@Icons.Material.Filled.ThumbDownAlt" Size="Size.Small" Color="Color.Dark"
OnClick="() => RemoveRequireEvent(e, context.Student, Requirement.Exclude)"></MudIconButton>
</MudTooltip>
}
</MudPaper>
}
<MudDivider Style="border-width:3px" />
</td></MudTr>
</ChildRowContent>
</MudTable>
</MudItem>
<MudItem xs="12" lg="4">
<MudText Typo="Typo.h4">Teams</MudText>
<MudTable T="Team" ServerData="SolveAssignments" @ref="_teamData">
<ColGroup>
<col style="width: 200px;" />
<col style="width: 40px; white-space:nowrap" />
</ColGroup>
<HeaderContent>
<MudTh>Team</MudTh>
<MudTh><MudTooltip Text="Number of Student Rankings, Number of TimeSlots [Eligibility Lower Bound-Upper Bound]"><MudText Style="white-space:nowrap;">R, # [LB-UB]</MudText> </MudTooltip></MudTh>
</HeaderContent>
<RowTemplate>
@{
var thresholds = _eventAssignmentThresholds.First(e => e.Event == context.Event);
}
<MudTd>
<b>@context.Event.Name</b>&nbsp;
<EventAttributes EventDefinition="context.Event"></EventAttributes>
</MudTd>
<MudTd Style="white-space:nowrap">
@thresholds.StudentRankingCount, [@thresholds.LowerBound-@thresholds.UpperBound] &times; @thresholds.TeamCount
@if (_eventTwoTeams.Contains(context.Event))
{
<MudIconButton Class="ml-3" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.PlusOne" Variant="Variant.Filled"
OnClick="() => RemoveTwoTeam(context.Event)"></MudIconButton>
}
else if (thresholds.TeamCount != 2 && context.Event.EventFormat == EventFormat.Team)
{
<MudIconButton Class="ml-3" Size="Size.Small" Color="Color.Default"
Icon="@Icons.Material.Filled.PlusOne"
OnClick="() => AddTwoTeam(context.Event)"></MudIconButton>
}
<MudIconButton Icon="@Icons.Material.Outlined.Remove" Size="Size.Small" Color="Color.Default"
OnClick="() => AddOmitted(context.Event)"></MudIconButton>
</MudTd>
</RowTemplate>
<ChildRowContent>
<MudTr>
<td colspan="2">
<TeamStudents Team="@context"></TeamStudents>
<MudDivider Style="border-width:3px"/>
</td>
</MudTr>
</ChildRowContent>
<NoRecordsContent>
<MudText Color="Color.Warning">Solution status: @_solutionStatus</MudText>
</NoRecordsContent>
<LoadingContent>
<MudText>Loading...</MudText>
</LoadingContent>
</MudTable>
</MudItem>
</MudGrid>
@if (_hasExistingTeams)
{
<MudAlert Severity="Severity.Warning" Class="ma-3" Variant="Variant.Filled">
<strong>Cannot save:</strong> There are existing teams. Please delete all teams before saving a new assignment.
<MudButton Href="/teams" Variant="Variant.Text" Color="Color.Default" Class="ml-2">View Teams</MudButton>
</MudAlert>
}
<MudTooltip Text="@(_hasExistingTeams ? "Delete existing teams first" : "This will save the team assignments")">
<MudButton Class="ma-3" StartIcon="@Icons.Material.Filled.Warning" Variant="Variant.Filled" Size="Size.Large"
OnClick="SaveTeams" Color="Color.Warning" Disabled="@_hasExistingTeams">Save</MudButton>
</MudTooltip>
@code {
public bool TestSwitch { get; set; } = false;
private readonly AssignmentParameters _parameters = new() { RequireOnSite = false, RequireRegional = false };
private List<EventDefinition>? _events;
private List<Student>? _students;
private List<EventAssignmentThresholds> _eventAssignmentThresholds = [];
MudTable<Team> _teamData;
private Team[] _teams = [];
MudTable<StudentEventStatistics> _statisticData;
private List<StudentEventStatistics> _statistics = [];
MudTable<AssignmentRequirement> _assignmentRequirementData;
private List<AssignmentRequirement> _assignmentRequirements = [];
MudTable<EventDefinition> _eventTwoTeamData;
private List<EventDefinition> _eventTwoTeams = [];
MudTable<EventDefinition> _eventOmittedData;
private List<EventDefinition> _eventOmitted = [];
private string _solutionStatus = string.Empty;
private bool _isSolving = false;
private bool _hasExistingTeams = 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();
// Check if there are existing teams
_hasExistingTeams = await Context.Teams.AnyAsync();
}
private async Task AddTeam()
{
//Context.TimeSlots.Add(Team);
await Context.SaveChangesAsync();
//NavigationManager.NavigateTo("/teams");
}
private void Solve()
{
_teamData.ReloadServerData();
}
private async Task<TableData<Team>> SolveAssignments(TableState arg1, CancellationToken arg2)
{
_isSolving = true;
var eventAssignment = new EventAssignment(_events, _students, _parameters);
foreach (var requirement in _assignmentRequirements)
{
eventAssignment.AddAssignmentRequirement(requirement);
}
eventAssignment.RemoveEvents(_eventOmitted);
eventAssignment.AllowTwoTeams(_eventTwoTeams);
var solution = await eventAssignment.Solve();
_solutionStatus = solution.Status;
_statistics =
StudentEventStatistics.Generate(solution.Teams)
.OrderByDescending(s => s.Student.Grade + s.Student.TsaYear)
.ThenBy(s => s.Student.FirstName).ToList();
_eventAssignmentThresholds = solution.AssignmentThresholds;
await _statisticData.ReloadServerData();
_isSolving = false;
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
_teams = solution.Teams;
return new TableData<Team> { Items = _teams };
}
private async Task<TableData<StudentEventStatistics>> ReloadStatistics(TableState arg1, CancellationToken arg2)
{
return new TableData<StudentEventStatistics> {Items = _statistics};
}
private async Task<TableData<AssignmentRequirement>> ReloadAssignmentRequirements(TableState arg1, CancellationToken arg2)
{
return new TableData<AssignmentRequirement> { Items = _assignmentRequirements };
}
private async Task<TableData<EventDefinition>> ReloadEventTwoTeam(TableState arg1, CancellationToken arg2)
{
return new TableData<EventDefinition> { Items = _eventTwoTeams };
}
private async Task<TableData<EventDefinition>> ReloadOmittedEvents(TableState arg1, CancellationToken arg2)
{
return new TableData<EventDefinition> { Items = _eventOmitted };
}
private void RequireEvent(EventDefinition evt, Student student, Requirement requirement)
{
_assignmentRequirements.Add(new AssignmentRequirement(evt, student, requirement));
_assignmentRequirementData.ReloadServerData();
}
private void RemoveRequireEvent(EventDefinition evt, Student student, Requirement requirement)
{
var assignmentRequirement =
_assignmentRequirements
.Find(ar => ar.EventDefinition == evt && ar.Student == student && ar.Requirement == requirement);
if (assignmentRequirement != null) RemoveRequireEvent(assignmentRequirement);
}
private void RemoveRequireEvent(AssignmentRequirement assignmentRequirement)
{
_assignmentRequirements.Remove(assignmentRequirement);
_assignmentRequirementData.ReloadServerData();
}
private void AddTwoTeam(EventDefinition evt)
{
_eventTwoTeams.Add(evt);
_eventTwoTeamData.ReloadServerData();
}
private void RemoveTwoTeam(EventDefinition evt)
{
_eventTwoTeams.Remove(evt);
_eventTwoTeamData.ReloadServerData();
}
private void AddOmitted(EventDefinition evt)
{
_eventOmitted.Add(evt);
_eventOmittedData.ReloadServerData();
}
private void RemoveOmitted(EventDefinition evt)
{
_eventOmitted.Remove(evt);
_eventOmittedData.ReloadServerData();
}
public async Task SaveTeams()
{
var result = await DialogService
.ShowMessageBox("Save Teams",
(MarkupString)$"Are you sure want to save these teams? Current teams will be erased. This cannot be undone.",
yesText: "Yes",
noText: "Cancel");
if (result == true)
{
await Context.Teams.ExecuteDeleteAsync();
await Context.Teams.AddRangeAsync(_teams);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/teams");
}
}
}