1601610226
This commit introduces the TeamMeetingToggleSelector component, which allows for the selection and management of teams within the meeting schedule. The Index.razor component has been updated to utilize this new selector, enhancing the user interface for managing scheduled and extended teams. Additionally, new methods for saving and loading extended teams have been added, improving the overall functionality and user experience in team scheduling. These changes contribute to better organization and management of team events in the application.
537 lines
20 KiB
Plaintext
537 lines
20 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"/>
|
|
<TeamMeetingToggleSelector Teams="@_teams"
|
|
SelectedTeams="_scheduledTeams"
|
|
SelectedTeamsChanged="OnScheduledTeamsChanged"
|
|
ExtendedTeams="_extendedTeams"
|
|
ExtendedTeamsChanged="OnExtendedTeamsChanged"
|
|
Title="Scheduled Teams"
|
|
ShowEventAttributes="false" />
|
|
</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 IEnumerable<Team> _extendedTeams = [];
|
|
|
|
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 Task OnExtendedTeamsChanged(IEnumerable<Team> teams)
|
|
{
|
|
_extendedTeams = teams;
|
|
await SaveExtendedTeams();
|
|
}
|
|
|
|
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();
|
|
await LoadExtendedTeams();
|
|
}
|
|
|
|
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 SaveExtendedTeams()
|
|
{
|
|
var teamIds = _extendedTeams.Select(t => t.Id).ToArray();
|
|
await LocalStorage.SetIntArrayAsync("MeetingSchedule_ExtendedTeams", teamIds);
|
|
}
|
|
|
|
private async Task LoadExtendedTeams()
|
|
{
|
|
var teamIds = await LocalStorage.GetIntArrayAsync("MeetingSchedule_ExtendedTeams");
|
|
if (teamIds.Length > 0)
|
|
{
|
|
_extendedTeams = _teams.Where(t => teamIds.Contains(t.Id)).ToArray();
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
// Update parameters with extended team event names
|
|
_parameters.ExtendedTeams = _extendedTeams
|
|
.Where(t => t.Event != null)
|
|
.Select(t => t.Event!.Name)
|
|
.ToArray();
|
|
|
|
var teamScheduler = new TeamScheduler(_scheduledTeams, _parameters.TimeSlots, availableStudents);
|
|
_solution = teamScheduler.Solve();
|
|
|
|
// Post-process: extend teams to next consecutive time slot
|
|
if (_extendedTeams.Any())
|
|
{
|
|
ExtendTeamsInSolution(_solution, _extendedTeams, availableStudents);
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
|
|
private void ExtendTeamsInSolution(TeamSchedulerSolution solution, IEnumerable<Team> extendedTeams, Student[] allStudents)
|
|
{
|
|
if (solution.TimeSlots == null || !solution.TimeSlots.Any())
|
|
return;
|
|
|
|
var extendedTeamsList = extendedTeams.ToList();
|
|
if (!extendedTeamsList.Any())
|
|
return;
|
|
|
|
var extendedTeamIds = extendedTeamsList.Select(t => t.Id).ToHashSet();
|
|
|
|
// Find which time slot each extended team is in
|
|
for (int slotIndex = 0; slotIndex < solution.TimeSlots.Length; slotIndex++)
|
|
{
|
|
var currentSlot = solution.TimeSlots[slotIndex];
|
|
var teamsToExtend = currentSlot.Teams.Where(t => extendedTeamIds.Contains(t.Id)).ToList();
|
|
|
|
if (!teamsToExtend.Any())
|
|
continue;
|
|
|
|
// Check if there's a next time slot
|
|
if (slotIndex + 1 >= solution.TimeSlots.Length)
|
|
{
|
|
// Cannot extend - this is the last time slot
|
|
continue;
|
|
}
|
|
|
|
var nextSlot = solution.TimeSlots[slotIndex + 1];
|
|
|
|
// Add extended teams to the next time slot
|
|
var nextSlotTeamsList = nextSlot.Teams.ToList();
|
|
var nextSlotTeamIds = nextSlotTeamsList.Select(t => t.Id).ToHashSet();
|
|
|
|
foreach (var team in teamsToExtend)
|
|
{
|
|
if (!nextSlotTeamIds.Contains(team.Id))
|
|
{
|
|
nextSlotTeamsList.Add(team);
|
|
nextSlotTeamIds.Add(team.Id);
|
|
}
|
|
}
|
|
|
|
// Update the next slot with the extended teams
|
|
nextSlot.Teams = nextSlotTeamsList.ToArray();
|
|
|
|
// Recalculate overlaps and unscheduled students for the next slot
|
|
nextSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(nextSlot.Teams);
|
|
nextSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(nextSlot.Teams, allStudents);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
} |