diff --git a/WebApp/Components/Features/MeetingSchedule/Index.razor b/WebApp/Components/Features/MeetingSchedule/Index.razor index 17de2ad..0581bd0 100644 --- a/WebApp/Components/Features/MeetingSchedule/Index.razor +++ b/WebApp/Components/Features/MeetingSchedule/Index.razor @@ -46,7 +46,13 @@ Reset - Solve + + Solve + @@ -120,75 +126,75 @@ private IEnumerable _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 teams) { _scheduledTeams = teams; - await SaveScheduledTeams(); + StateHasChanged(); } private async Task OnAbsentStudentsChanged(IEnumerable students) { _absentStudents = students; - await SaveAbsentStudents(); + StateHasChanged(); } private async Task OnTimeSlotCountChanged(int timeSlots) { _parameters.TimeSlots = timeSlots; - await SaveTimeSlotCount(); + StateHasChanged(); } private async Task OnExtendedTeamsChanged(IEnumerable 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> 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 + { + public HashSet ScheduledTeamIds { get; set; } = []; + public HashSet AbsentStudentIds { get; set; } = []; + public int TimeSlotCount { get; set; } + public HashSet 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 scheduledTeams, + IEnumerable absentStudents, + int timeSlotCount, + IEnumerable 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 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("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); + } + } + } \ No newline at end of file