f8c22690d4
This commit introduces two new utility classes: StudentNameFormatter and TeamStudentNameFormatter, which provide methods for formatting student names with options for indicating absence and overlaps. The Team class has been updated to remove the StudentsFirstNames property, and various components across the WebApp have been refactored to utilize the new formatting utilities. This enhances the maintainability and readability of the code while improving the presentation of student names in the UI.
449 lines
17 KiB
Plaintext
449 lines
17 KiB
Plaintext
@page "/meeting-schedule"
|
|
@attribute [Authorize]
|
|
@using System.Text
|
|
@using Core.Calculation
|
|
@using Microsoft.EntityFrameworkCore
|
|
@using WebApp.Components.Shared.Components
|
|
@using Core.Utility
|
|
@inject IConfiguration Configuration
|
|
@inject AppDbContext Context
|
|
@inject ClipboardService ClipboardService
|
|
@inject LocalStorageService LocalStorage
|
|
|
|
<PageHeader Title="@($"{Configuration["ChapterSettings:Shortname"]} TSA Schedule {Configuration["ChapterSettings:CompetitionYear"]}")" />
|
|
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6 mt-4">
|
|
<MudGrid>
|
|
<MudItem xs="12" sm="8" lg="9">
|
|
<MudText Typo="Typo.h4">Time Slots</MudText>
|
|
<MudPaper Class="pa-2 ma-2" Elevation="3">
|
|
<MudGrid>
|
|
<MudItem xs="6" sm="3" lg="2">
|
|
<MudNumericField Value="_parameters.TimeSlots"
|
|
ValueChanged="async (int val) => await OnTimeSlotCountChanged(val)"
|
|
Label="Time Slots" Min="1" Max="4">
|
|
</MudNumericField>
|
|
</MudItem>
|
|
<MudFlexBreak/>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudTooltip Text="Schedule teams with Level of Effort >= 3" Inline="false">
|
|
<MudButton Variant="Variant.Outlined" OnClick="AddHighLevelOfEffort" FullWidth="true">Add High Effort</MudButton>
|
|
</MudTooltip>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" OnClick="AddRegionals" FullWidth="true">Add Regionals</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" OnClick="RemoveIndividual" FullWidth="true">Remove Individual</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" OnClick="RemoveLowLevelOfEffort" FullWidth="true">Remove Low Effort</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" OnClick="Invert" FullWidth="true">Invert</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="6" lg="4">
|
|
<MudButton Variant="Variant.Outlined" Color="Color.Warning" OnClick="Reset" FullWidth="true">Reset</MudButton>
|
|
</MudItem>
|
|
<MudItem xs="12">
|
|
<MudButton Variant="Variant.Filled" Class="ma-3" OnClick="Solve" Color="Color.Primary" Disabled="@_isSolving">Solve</MudButton>
|
|
<MudTooltip Text="Copy to Clipboard">
|
|
<MudIconButton OnClick="CopyToClipboard" Icon="@Icons.Material.Filled.ContentCopy"></MudIconButton>
|
|
</MudTooltip>
|
|
</MudItem>
|
|
|
|
</MudGrid>
|
|
</MudPaper>
|
|
|
|
<MudTable T="TeamScheduleTimeSlot" ServerData="SolveSchedule" @ref="_solutionData">
|
|
<HeaderContent>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>
|
|
<MudGrid>
|
|
<MudItem xs="12" lg="6">
|
|
<ScheduledTeamsList TimeSlotName="@context.Name"
|
|
Teams="@context.Teams"
|
|
ScheduledTeams="@_scheduledTeams"
|
|
AbsentStudents="@_absentStudents"
|
|
StudentHasOverlaps="@context.StudentHasOverlaps"
|
|
OnToggleTeam="@ToggleRequiredTeam" />
|
|
</MudItem>
|
|
<MudItem xs="12" lg="6">
|
|
<UnscheduledStudentsList UnscheduledStudents="@context.UnscheduledStudents"
|
|
AbsentStudents="@_absentStudents"
|
|
ScheduledTeams="@_scheduledTeams"
|
|
PossibleAdditions="@_possibleAdditions"
|
|
UnassignedTeams="@_solution.StudentUnassignedTeams"
|
|
OnToggleTeam="@ToggleRequiredTeam" />
|
|
</MudItem>
|
|
</MudGrid>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudItem>
|
|
<MudItem xs="12" sm="4" lg="3">
|
|
<MudStack>
|
|
<StudentTextBoxSelector Students="@_students"
|
|
SelectedStudents="_absentStudents"
|
|
SelectedStudentsChanged="OnAbsentStudentsChanged"
|
|
Title="Absent Students"
|
|
Label="Search for absent students"
|
|
ShowFullName="true"/>
|
|
<MudDivider Class="my-4"/>
|
|
<TeamToggleSelector Teams="@_teams"
|
|
SelectedTeams="_scheduledTeams"
|
|
SelectedTeamsChanged="OnScheduledTeamsChanged"
|
|
Title="Scheduled Teams"
|
|
ShowEventAttributes="true" />
|
|
</MudStack>
|
|
</MudItem>
|
|
|
|
</MudGrid>
|
|
</MudPaper>
|
|
|
|
@code {
|
|
private Team[] _teams = null!;
|
|
private Student[] _students = null!;
|
|
MudTable<TeamScheduleTimeSlot> _solutionData = null!;
|
|
private TeamSchedulerSolution _solution = null!;
|
|
private TeamSchedulerOptions _parameters = null!;
|
|
bool _isSolving;
|
|
private IEnumerable<Team> _scheduledTeams = [];
|
|
private IEnumerable<Student> _absentStudents = [];
|
|
private IEnumerable<Team> _possibleAdditions = [];
|
|
|
|
private async Task OnScheduledTeamsChanged(IEnumerable<Team> teams)
|
|
{
|
|
_scheduledTeams = teams;
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async Task OnAbsentStudentsChanged(IEnumerable<Student> students)
|
|
{
|
|
_absentStudents = students;
|
|
await SaveAbsentStudents();
|
|
}
|
|
|
|
private async Task OnTimeSlotCountChanged(int timeSlots)
|
|
{
|
|
_parameters.TimeSlots = timeSlots;
|
|
await SaveTimeSlotCount();
|
|
}
|
|
|
|
private async void AddRegionals()
|
|
{
|
|
_scheduledTeams
|
|
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async void AddHighLevelOfEffort()
|
|
{
|
|
_scheduledTeams
|
|
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async void RemoveIndividual()
|
|
{
|
|
_scheduledTeams
|
|
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async void RemoveLowLevelOfEffort()
|
|
{
|
|
_scheduledTeams
|
|
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async void Invert()
|
|
{
|
|
var rt = _scheduledTeams.ToArray();
|
|
_scheduledTeams
|
|
= _teams.Where(t => !rt.Contains(t));
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async void Reset()
|
|
{
|
|
_scheduledTeams = [];
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
private async void ToggleRequiredTeam(Team unassignedTeam)
|
|
{
|
|
var scheduledTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
|
if (scheduledTeamIds.Contains(unassignedTeam.Id))
|
|
_scheduledTeams = _scheduledTeams.Where(t => t.Id != unassignedTeam.Id);
|
|
else
|
|
{
|
|
_scheduledTeams = _scheduledTeams.Concat([unassignedTeam]);
|
|
}
|
|
await SaveScheduledTeams();
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_parameters =
|
|
new TeamSchedulerOptions(
|
|
2,
|
|
mustIncludeEvents:
|
|
[
|
|
// "Medical Technology", "Electrical Applications" , "RegionalTeam",
|
|
// ,"Dragster", "Flight"
|
|
],
|
|
extended:
|
|
[
|
|
// "Invention", "Construction Challenge", "Mechanical", "Mass", "Micro"
|
|
//"STEM"
|
|
//"Community", "Vlogging"// "Microcontroller"
|
|
],
|
|
omittedEvents:
|
|
[
|
|
// "Vlogging", "Junior", "Community Service Video", "Digital Photography",
|
|
// "STEM"
|
|
|
|
//"Leadership",// "Electrical", //"Construction"
|
|
// "Forensic",
|
|
//"CAD"
|
|
//"I&I Team 1", "I&I Team 2"//, "Website Design",
|
|
],
|
|
absentStudents:
|
|
[
|
|
]
|
|
);
|
|
|
|
_teams
|
|
= await Context.Teams
|
|
.AsNoTracking()
|
|
.Include(e => e.Event)
|
|
.Include(e => e.Students)
|
|
.OrderBy(e => e.Event.Name)
|
|
.ThenBy(e => e.Identifier)
|
|
.ToArrayAsync();
|
|
|
|
_students =
|
|
await Context.Students
|
|
.AsNoTracking()
|
|
.Include(e => e.Teams)
|
|
.ThenInclude(t => t.Event)
|
|
.Include(e => e.Teams)
|
|
.ThenInclude(t => t.Captain)
|
|
.Include(e => e.EventRankings)
|
|
.ThenInclude(e => e.EventDefinition)
|
|
.OrderBy(e => e.FirstName).ToArrayAsync();
|
|
|
|
// Load saved selections from localStorage
|
|
await LoadScheduledTeams();
|
|
await LoadAbsentStudents();
|
|
await LoadTimeSlotCount();
|
|
}
|
|
|
|
private async Task SaveScheduledTeams()
|
|
{
|
|
var teamIds = _scheduledTeams.Select(t => t.Id).ToArray();
|
|
await LocalStorage.SetIntArrayAsync("MeetingSchedule_ScheduledTeams", teamIds);
|
|
}
|
|
|
|
private async Task LoadScheduledTeams()
|
|
{
|
|
var teamIds = await LocalStorage.GetIntArrayAsync("MeetingSchedule_ScheduledTeams");
|
|
if (teamIds.Length > 0)
|
|
{
|
|
_scheduledTeams = _teams.Where(t => teamIds.Contains(t.Id)).ToArray();
|
|
}
|
|
}
|
|
|
|
private async Task SaveAbsentStudents()
|
|
{
|
|
var studentIds = _absentStudents.Select(s => s.Id).ToArray();
|
|
await LocalStorage.SetIntArrayAsync("MeetingSchedule_AbsentStudents", studentIds);
|
|
}
|
|
|
|
private async Task LoadAbsentStudents()
|
|
{
|
|
var studentIds = await LocalStorage.GetIntArrayAsync("MeetingSchedule_AbsentStudents");
|
|
if (studentIds.Length > 0)
|
|
{
|
|
_absentStudents = _students.Where(s => studentIds.Contains(s.Id)).ToArray();
|
|
}
|
|
}
|
|
|
|
private async Task SaveTimeSlotCount()
|
|
{
|
|
await LocalStorage.SetIntAsync("MeetingSchedule_TimeSlotCount", _parameters.TimeSlots);
|
|
}
|
|
|
|
private async Task LoadTimeSlotCount()
|
|
{
|
|
var timeSlots = await LocalStorage.GetIntAsync("MeetingSchedule_TimeSlotCount", defaultValue: 2);
|
|
if (timeSlots > 0)
|
|
{
|
|
_parameters.TimeSlots = timeSlots;
|
|
}
|
|
}
|
|
|
|
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
|
|
{
|
|
_isSolving = true;
|
|
|
|
// Check if there are any teams to schedule
|
|
if (!_scheduledTeams.Any())
|
|
{
|
|
_isSolving = false;
|
|
_solution = new TeamSchedulerSolution([], [], "No teams selected");
|
|
return new TableData<TeamScheduleTimeSlot> { Items = [] };
|
|
}
|
|
|
|
// Filter out absent students
|
|
var availableStudents = _students.Where(s => !_absentStudents.Contains(s)).ToArray();
|
|
|
|
// Check if there are any available students
|
|
if (availableStudents.Length == 0)
|
|
{
|
|
_isSolving = false;
|
|
_solution = new TeamSchedulerSolution([], [], "No available students");
|
|
return new TableData<TeamScheduleTimeSlot> { Items = [] };
|
|
}
|
|
|
|
// Update parameters with absent student names
|
|
_parameters.AbsentStudents = _absentStudents.Select(s => s.FirstNameLastName).ToArray();
|
|
|
|
var teamScheduler = new TeamScheduler(_scheduledTeams, _parameters.TimeSlots, availableStudents);
|
|
_solution = teamScheduler.Solve();
|
|
|
|
// Try recommendation strategies in priority order
|
|
var scheduler = new UnassignedStudentScheduler(_teams, _solution.TimeSlots);
|
|
UnassignedScheduleStrategy[] strategies =
|
|
[
|
|
UnassignedScheduleStrategy.LevelOfEffort,
|
|
UnassignedScheduleStrategy.BiggestGroup,
|
|
UnassignedScheduleStrategy.AnyNotMeetingAlready,
|
|
UnassignedScheduleStrategy.IndividualEvents
|
|
];
|
|
|
|
_possibleAdditions = strategies
|
|
.Select(strategy => scheduler.ScheduleStrategy(strategy))
|
|
.FirstOrDefault(result => result.Any()) ?? [];
|
|
|
|
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
|
|
|
|
_isSolving = false;
|
|
return new TableData<TeamScheduleTimeSlot> { Items = _solution.TimeSlots };
|
|
}
|
|
|
|
private void Solve()
|
|
{
|
|
_solutionData.ReloadServerData();
|
|
}
|
|
|
|
async Task CopyToClipboard()
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var timeslot in _solution.TimeSlots)
|
|
{
|
|
AppendScheduledTeams(sb, timeslot);
|
|
AppendUnscheduledStudents(sb, timeslot);
|
|
sb.Append(Environment.NewLine);
|
|
}
|
|
|
|
try
|
|
{
|
|
await ClipboardService.WriteTextAsync(sb.ToString());
|
|
}
|
|
catch
|
|
{
|
|
Console.WriteLine("Cannot write text to clipboard");
|
|
}
|
|
}
|
|
|
|
private void AppendScheduledTeams(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
|
{
|
|
foreach (var scheduledTeam in timeslot.Teams.OrderBy(e => e.ToString()))
|
|
{
|
|
var teamName = scheduledTeam.ToString();
|
|
|
|
if (scheduledTeam.Event.EventFormat is EventFormat.Individual)
|
|
{
|
|
sb.Append(teamName);
|
|
}
|
|
else
|
|
{
|
|
var studentsList = FormatStudentList(scheduledTeam, timeslot);
|
|
sb.Append($"{teamName} - {studentsList}");
|
|
}
|
|
sb.Append(Environment.NewLine);
|
|
}
|
|
}
|
|
|
|
private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot)
|
|
{
|
|
return TeamStudentNameFormatter.FormatStudentList(
|
|
team,
|
|
new TeamStudentNameFormatter.FormatOptions
|
|
{
|
|
Ordering = TeamStudentNameFormatter.OrderingStyle.CaptainFirst,
|
|
MarkOverlaps = true,
|
|
HasOverlaps = timeslot.StudentHasOverlaps,
|
|
MarkAbsent = true,
|
|
AbsentStudents = _absentStudents.ToList()
|
|
});
|
|
}
|
|
|
|
private string FormatStudentName(Student student, TeamScheduleTimeSlot timeslot)
|
|
{
|
|
// Find the team this student belongs to for formatting context
|
|
var team = _teams.FirstOrDefault(t => t.Students.Contains(student));
|
|
if (team == null)
|
|
{
|
|
// No team context, use StudentNameFormatter directly
|
|
return StudentNameFormatter.FormatStudentName(
|
|
student,
|
|
new StudentNameFormatter.FormatOptions
|
|
{
|
|
HasOverlap = timeslot.StudentHasOverlaps(student),
|
|
IsAbsent = _absentStudents.Contains(student)
|
|
});
|
|
}
|
|
|
|
return TeamStudentNameFormatter.FormatStudentName(
|
|
student,
|
|
team,
|
|
new TeamStudentNameFormatter.FormatOptions
|
|
{
|
|
MarkOverlaps = true,
|
|
HasOverlaps = timeslot.StudentHasOverlaps,
|
|
MarkAbsent = true,
|
|
AbsentStudents = _absentStudents.ToList()
|
|
});
|
|
}
|
|
|
|
private void AppendUnscheduledStudents(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
|
{
|
|
if (!timeslot.UnscheduledStudents.Any())
|
|
return;
|
|
|
|
sb.Append("--Unscheduled");
|
|
sb.Append(Environment.NewLine);
|
|
|
|
foreach (var student in timeslot.UnscheduledStudents)
|
|
{
|
|
var studentName = StudentNameFormatter.FormatStudentName(
|
|
student,
|
|
new StudentNameFormatter.FormatOptions
|
|
{
|
|
IsAbsent = _absentStudents.Contains(student)
|
|
});
|
|
|
|
var unassignedTeams = _solution.StudentUnassignedTeams(student);
|
|
var teamsList = string.Join(", ", unassignedTeams.Select(e => e.ToString()));
|
|
|
|
sb.Append($"{studentName} - {teamsList}");
|
|
sb.Append(Environment.NewLine);
|
|
}
|
|
}
|
|
|
|
} |