Implement state management for MeetingSchedule component with local storage support

This commit enhances the Index.razor component of the MeetingSchedule feature by introducing a new MeetingScheduleState class to manage the scheduling state. The component now tracks the last saved state for dirty/clean comparisons and saves the current state to local storage after solving the schedule. Additionally, the UI has been updated to reflect changes in the button's appearance based on the dirty state, improving user feedback and interaction. These changes contribute to a more robust and user-friendly scheduling experience.
This commit is contained in:
2026-01-13 12:50:34 -05:00
parent 8c4c21f204
commit 34658e9697
@@ -46,7 +46,13 @@
<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>
<MudButton Variant="@(IsDirty() ? Variant.Outlined : 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>
@@ -120,75 +126,75 @@
private IEnumerable<Team> _extendedTeams = [];
// Key: (teamId, timeSlotIndex, studentId) - Value: true if excluded
private Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> _excludedStudents = new();
// Track last saved state for dirty/clean comparison
private MeetingScheduleState? _lastSavedState;
private async Task OnScheduledTeamsChanged(IEnumerable<Team> teams)
{
_scheduledTeams = teams;
await SaveScheduledTeams();
StateHasChanged();
}
private async Task OnAbsentStudentsChanged(IEnumerable<Student> students)
{
_absentStudents = students;
await SaveAbsentStudents();
StateHasChanged();
}
private async Task OnTimeSlotCountChanged(int timeSlots)
{
_parameters.TimeSlots = timeSlots;
await SaveTimeSlotCount();
StateHasChanged();
}
private async Task OnExtendedTeamsChanged(IEnumerable<Team> teams)
{
_extendedTeams = teams;
await SaveExtendedTeams();
StateHasChanged();
}
private async void AddRegionals()
private void AddRegionals()
{
_scheduledTeams
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
await SaveScheduledTeams();
StateHasChanged();
}
private async void AddHighLevelOfEffort()
private void AddHighLevelOfEffort()
{
_scheduledTeams
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
await SaveScheduledTeams();
StateHasChanged();
}
private async void RemoveIndividual()
private void RemoveIndividual()
{
_scheduledTeams
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
await SaveScheduledTeams();
StateHasChanged();
}
private async void RemoveLowLevelOfEffort()
private void RemoveLowLevelOfEffort()
{
_scheduledTeams
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
await SaveScheduledTeams();
StateHasChanged();
}
private async void Invert()
private void Invert()
{
var rt = _scheduledTeams.ToArray();
_scheduledTeams
= _teams.Where(t => !rt.Contains(t));
await SaveScheduledTeams();
StateHasChanged();
}
private async void Reset()
private void Reset()
{
_scheduledTeams = [];
_extendedTeams = [];
_excludedStudents.Clear();
await SaveScheduledTeams();
await SaveExtendedTeams();
await SaveExcludedStudents();
StateHasChanged();
}
private async Task ToggleRequiredTeam(Team unassignedTeam)
@@ -264,6 +270,19 @@
await LoadTimeSlotCount();
await LoadExtendedTeams();
await LoadExcludedStudents();
// Initialize last saved state from loaded values
_lastSavedState = await MeetingScheduleState.FromLocalStorage(LocalStorage, _teams, _students);
if (_lastSavedState == null)
{
// If no saved state exists, create initial state from current values
_lastSavedState = MeetingScheduleState.FromCurrent(
_scheduledTeams,
_absentStudents,
_parameters.TimeSlots,
_extendedTeams,
_excludedStudents);
}
}
private async Task SaveScheduledTeams()
@@ -368,7 +387,6 @@
{
_excludedStudents[key] = true;
}
await SaveExcludedStudents();
StateHasChanged();
}
@@ -409,6 +427,21 @@
private record ExcludedStudent(int TeamId, int TimeSlotIndex, int StudentId);
private bool IsDirty()
{
if (_lastSavedState == null)
return true;
var currentState = MeetingScheduleState.FromCurrent(
_scheduledTeams,
_absentStudents,
_parameters.TimeSlots,
_extendedTeams,
_excludedStudents);
return !currentState.Equals(_lastSavedState);
}
private async Task<TableData<TeamScheduleTimeSlot>> SolveSchedule(TableState arg1, CancellationToken arg2)
{
_isSolving = true;
@@ -516,6 +549,16 @@
.Select(strategy => scheduler.ScheduleStrategy(strategy))
.FirstOrDefault(result => result.Any()) ?? [];
// Save state to localStorage after solving completes successfully
var currentState = MeetingScheduleState.FromCurrent(
_scheduledTeams,
_absentStudents,
_parameters.TimeSlots,
_extendedTeams,
_excludedStudents);
await currentState.SaveToLocalStorage(LocalStorage);
_lastSavedState = currentState;
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
_isSolving = false;
@@ -726,4 +769,113 @@
}
}
private class MeetingScheduleState : IEquatable<MeetingScheduleState>
{
public HashSet<int> ScheduledTeamIds { get; set; } = [];
public HashSet<int> AbsentStudentIds { get; set; } = [];
public int TimeSlotCount { get; set; }
public HashSet<int> ExtendedTeamIds { get; set; } = [];
public HashSet<(int teamId, int timeSlotIndex, int studentId)> ExcludedStudents { get; set; } = [];
public bool Equals(MeetingScheduleState? other)
{
if (other == null) return false;
return ScheduledTeamIds.SetEquals(other.ScheduledTeamIds) &&
AbsentStudentIds.SetEquals(other.AbsentStudentIds) &&
TimeSlotCount == other.TimeSlotCount &&
ExtendedTeamIds.SetEquals(other.ExtendedTeamIds) &&
ExcludedStudents.SetEquals(other.ExcludedStudents);
}
public override bool Equals(object? obj) => Equals(obj as MeetingScheduleState);
public override int GetHashCode()
{
var hash = new HashCode();
hash.Add(ScheduledTeamIds.Count);
foreach (var id in ScheduledTeamIds.OrderBy(x => x))
hash.Add(id);
hash.Add(AbsentStudentIds.Count);
foreach (var id in AbsentStudentIds.OrderBy(x => x))
hash.Add(id);
hash.Add(TimeSlotCount);
hash.Add(ExtendedTeamIds.Count);
foreach (var id in ExtendedTeamIds.OrderBy(x => x))
hash.Add(id);
hash.Add(ExcludedStudents.Count);
foreach (var key in ExcludedStudents.OrderBy(x => x))
hash.Add(key);
return hash.ToHashCode();
}
public static MeetingScheduleState FromCurrent(
IEnumerable<Team> scheduledTeams,
IEnumerable<Student> absentStudents,
int timeSlotCount,
IEnumerable<Team> extendedTeams,
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents)
{
return new MeetingScheduleState
{
ScheduledTeamIds = scheduledTeams.Select(t => t.Id).ToHashSet(),
AbsentStudentIds = absentStudents.Select(s => s.Id).ToHashSet(),
TimeSlotCount = timeSlotCount,
ExtendedTeamIds = extendedTeams.Select(t => t.Id).ToHashSet(),
ExcludedStudents = excludedStudents.Keys
.Where(k => excludedStudents[k])
.ToHashSet()
};
}
public static async Task<MeetingScheduleState?> FromLocalStorage(
LocalStorageService localStorage,
Team[] allTeams,
Student[] allStudents)
{
// Load scheduled teams
var scheduledTeamIds = await localStorage.GetIntArrayAsync("MeetingSchedule_ScheduledTeams");
var absentStudentIds = await localStorage.GetIntArrayAsync("MeetingSchedule_AbsentStudents");
var timeSlotCount = await localStorage.GetIntAsync("MeetingSchedule_TimeSlotCount", defaultValue: 2);
var extendedTeamIds = await localStorage.GetIntArrayAsync("MeetingSchedule_ExtendedTeams");
var exclusions = await localStorage.GetJsonAsync<ExcludedStudent[]>("MeetingSchedule_ExcludedStudents");
// If no state exists, return null
if (scheduledTeamIds.Length == 0 && absentStudentIds.Length == 0 &&
timeSlotCount == 2 && extendedTeamIds.Length == 0 &&
(exclusions == null || exclusions.Length == 0))
{
return null;
}
var excludedStudentsSet = new HashSet<(int teamId, int timeSlotIndex, int studentId)>();
if (exclusions != null && exclusions.Length > 0)
{
foreach (var exclusion in exclusions)
{
excludedStudentsSet.Add((exclusion.TeamId, exclusion.TimeSlotIndex, exclusion.StudentId));
}
}
return new MeetingScheduleState
{
ScheduledTeamIds = scheduledTeamIds.ToHashSet(),
AbsentStudentIds = absentStudentIds.ToHashSet(),
TimeSlotCount = timeSlotCount > 0 ? timeSlotCount : 2,
ExtendedTeamIds = extendedTeamIds.ToHashSet(),
ExcludedStudents = excludedStudentsSet
};
}
public async Task SaveToLocalStorage(LocalStorageService localStorage)
{
await localStorage.SetIntArrayAsync("MeetingSchedule_ScheduledTeams", ScheduledTeamIds.ToArray());
await localStorage.SetIntArrayAsync("MeetingSchedule_AbsentStudents", AbsentStudentIds.ToArray());
await localStorage.SetIntAsync("MeetingSchedule_TimeSlotCount", TimeSlotCount);
await localStorage.SetIntArrayAsync("MeetingSchedule_ExtendedTeams", ExtendedTeamIds.ToArray());
var exclusions = ExcludedStudents.Select(e => new ExcludedStudent(e.teamId, e.timeSlotIndex, e.studentId)).ToArray();
await localStorage.SetJsonAsync("MeetingSchedule_ExcludedStudents", exclusions);
}
}
}