@page "/meeting-schedule" @attribute [Authorize] @using System.Text @using Core.Calculation @using Microsoft.EntityFrameworkCore @using WebApp.Components.Shared.Components @inject IConfiguration Configuration @inject AppDbContext Context @inject ClipboardService ClipboardService @inject LocalStorageService LocalStorage Time Slots Add High Effort Add Regionals Remove Individual Remove Low Effort Invert Reset Solve @code { private Team[] _teams = null!; private Student[] _students = null!; MudTable _solutionData = null!; private TeamSchedulerSolution _solution = null!; private TeamSchedulerOptions _parameters = null!; bool _isSolving; private IEnumerable _scheduledTeams = []; private IEnumerable _absentStudents = []; private IEnumerable _possibleAdditions = []; private async Task OnScheduledTeamsChanged(IEnumerable teams) { _scheduledTeams = teams; await SaveScheduledTeams(); } private async Task OnAbsentStudentsChanged(IEnumerable 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) { if (_scheduledTeams.Contains(unassignedTeam)) _scheduledTeams = _scheduledTeams.Where(t => t != unassignedTeam); else { _scheduledTeams = _scheduledTeams.Concat(new[] { 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 .Include(e => e.Event) .Include(e => e.Students) .OrderBy(e => e.Event.Name) .ThenBy(e => e.Identifier) .ToArrayAsync(); _students = await Context.Students .Include(e => e.Teams) .ThenInclude(e => e.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> 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 { 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 { 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); var strategies = new[] { 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 { 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 string.Join(", ", team.Students .OrderBy(e => e == team.Captain) .ThenBy(e => e.FirstName) .Select(e => FormatStudentName(e, timeslot))); } private string FormatStudentName(Student student, TeamScheduleTimeSlot timeslot) { var name = student.FirstName; if (timeslot.StudentHasOverlaps(student)) name += "*"; if (_absentStudents.Contains(student)) name += " (absent)"; return name; } 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 = student.FirstName; if (_absentStudents.Contains(student)) studentName += " (absent)"; var unassignedTeams = _solution.StudentUnassignedTeams(student); var teamsList = string.Join(", ", unassignedTeams.Select(e => e.ToString())); sb.Append($"{studentName} - {teamsList}"); sb.Append(Environment.NewLine); } } }