68311f4012
This commit updates the ScheduledTeamsList.razor component by replacing the existing MudChip implementation with InteractiveChip for better user interaction when managing student exclusions. The logic for displaying excluded students has been simplified, improving the overall readability and maintainability of the code. Additionally, the hover functionality has been removed to streamline the user experience. These changes contribute to a more intuitive and responsive scheduling interface.
489 lines
22 KiB
Plaintext
489 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-3 pa-md-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-3 pa-md-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 = "border-style: solid;";
|
|
|
|
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};";
|
|
}
|
|
|
|
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;
|
|
|
|
<InteractiveChip WrapperClass="mx-3 my-1" Style="@style" Variant="Variant.Text">
|
|
<ChildContent>
|
|
@e.ShortName
|
|
@AppIcons.EventAttributes(e)
|
|
</ChildContent>
|
|
<ControlContent>
|
|
@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>
|
|
}
|
|
</ControlContent>
|
|
</InteractiveChip>
|
|
}
|
|
<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>
|
|
<EventAttributes EventDefinition="context.Event"></EventAttributes>
|
|
</MudTd>
|
|
<MudTd Style="white-space:nowrap">
|
|
|
|
@thresholds.StudentRankingCount, [@thresholds.LowerBound-@thresholds.UpperBound] × @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");
|
|
}
|
|
}
|
|
} |