Files
chapter-organizer/WebApp/Components/Features/Teams/Assignment.razor
T
poprhythm 5c1e0b7444 Update padding for MudPaper components across various features
Increased padding from 'pa-4' to 'pa-6' for MudPaper components in Events, MeetingSchedule, Students, and Teams features to ensure consistent styling and improved visual spacing.
2025-12-27 10:43:15 -05:00

486 lines
22 KiB
Plaintext

@page "/teams/assignment"
@attribute [Authorize]
@using Core.Calculation
@using Core.Validation
@using Microsoft.EntityFrameworkCore
@using WebApp.Models
@using WebApp.Components.Shared.Components
@using EventAssignment = Core.Calculation.EventAssignment
@inject AppDbContext Context
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject ValidationService ValidationService
<PageHeader
Title="Assignment"
Description="Optimized team assignments based on the student event rankings">
<ActionButtons>
<MudButton StartIcon="@Icons.Material.Filled.Edit" Href="students/event-ranking" Variant="Variant.Outlined">
Edit Student Event Rankings
</MudButton>
</ActionButtons>
</PageHeader>
<MudPaper Elevation="2" Class="pa-6 mt-4">
<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>
<MudPaper Elevation="2" Class="pa-6 mt-4">
<MudGrid>
<MudItem xs="12" lg="8">
<MudText Typo="Typo.h5" Class="mb-4">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>
<ValidationWarnings Warnings="@GetStatisticsWarnings(context)" />
</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.h5" Class="mb-4">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>
</MudPaper>
@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 ValidationService _validationService = new ValidationService(ValidationConfiguration.Default);
private List<EventDefinition> _events = null!;
private List<Student> _students = null!;
private List<EventAssignmentThresholds> _eventAssignmentThresholds = [];
MudTable<Team> _teamData = null!;
private Team[] _teams = [];
MudTable<StudentEventStatistics> _statisticData = null!;
private List<StudentEventStatistics> _statistics = [];
MudTable<AssignmentRequirement> _assignmentRequirementData = null!;
private List<AssignmentRequirement> _assignmentRequirements = [];
MudTable<EventDefinition> _eventTwoTeamData = null!;
private List<EventDefinition> _eventTwoTeams = [];
MudTable<EventDefinition> _eventOmittedData = null!;
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;
UpdateValidationConfig();
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 Task<TableData<StudentEventStatistics>> ReloadStatistics(TableState arg1, CancellationToken arg2)
{
return Task.FromResult(new TableData<StudentEventStatistics> {Items = _statistics});
}
private void UpdateValidationConfig()
{
var config = ValidationConfiguration.FromAssignmentParameters(_parameters);
_validationService = new ValidationService(config);
}
private List<ValidationWarning> GetStatisticsWarnings(StudentEventStatistics stats)
{
return _validationService.ValidateStudentStatistics(stats, ValidationContext.StudentAssignment);
}
private Task<TableData<AssignmentRequirement>> ReloadAssignmentRequirements(TableState arg1, CancellationToken arg2)
{
return Task.FromResult(new TableData<AssignmentRequirement> { Items = _assignmentRequirements });
}
private Task<TableData<EventDefinition>> ReloadEventTwoTeam(TableState arg1, CancellationToken arg2)
{
return Task.FromResult(new TableData<EventDefinition> { Items = _eventTwoTeams });
}
private Task<TableData<EventDefinition>> ReloadOmittedEvents(TableState arg1, CancellationToken arg2)
{
return Task.FromResult(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");
}
}
}