Add meeting schedule state management services and related models
This commit introduces several new services and models to manage the meeting schedule state within localStorage. The MeetingScheduleState class is created to track scheduled teams, absent students, time slot counts, extended teams, and excluded students. Additionally, the IMeetingScheduleStateService interface and its implementation, MeetingScheduleStateService, are added to handle loading and saving state data. The MeetingScheduleClipboardService and MeetingScheduleDataService are also introduced to facilitate clipboard operations and data loading from the database, respectively. These enhancements improve the overall functionality and user experience of the meeting scheduling feature.
This commit is contained in:
@@ -0,0 +1,58 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
|
||||||
|
namespace Core.Calculation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper methods for calculating overlaps in team scheduling solutions.
|
||||||
|
/// </summary>
|
||||||
|
public static class OverlapCalculationHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates teams with excluded students and absent students filtered out for overlap calculation.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="teams">The teams to filter</param>
|
||||||
|
/// <param name="timeSlotIndex">The time slot index for exclusion lookups</param>
|
||||||
|
/// <param name="excludedStudents">Dictionary of excluded students: key is (teamId, timeSlotIndex, studentId), value is true if excluded</param>
|
||||||
|
/// <param name="absentStudentIds">Set of absent student IDs to exclude from overlap calculations</param>
|
||||||
|
/// <returns>Teams with excluded and absent students removed</returns>
|
||||||
|
public static Team[] GetTeamsWithoutExcludedStudents(
|
||||||
|
Team[] teams,
|
||||||
|
int timeSlotIndex,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents,
|
||||||
|
HashSet<int> absentStudentIds)
|
||||||
|
{
|
||||||
|
return teams.Select(team =>
|
||||||
|
{
|
||||||
|
// Find excluded students for this team in this time slot
|
||||||
|
// Also exclude absent students from overlap calculations
|
||||||
|
var includedStudents = team.Students
|
||||||
|
.Where(s => !IsStudentExcluded(team.Id, timeSlotIndex, s.Id, excludedStudents) &&
|
||||||
|
!absentStudentIds.Contains(s.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// If no students are excluded, return original team
|
||||||
|
if (includedStudents.Count == team.Students.Count)
|
||||||
|
return team;
|
||||||
|
|
||||||
|
// Create a temporary team with excluded and absent students removed
|
||||||
|
return new Team
|
||||||
|
{
|
||||||
|
Id = team.Id,
|
||||||
|
Event = team.Event,
|
||||||
|
Students = includedStudents,
|
||||||
|
Captain = team.Captain,
|
||||||
|
Identifier = team.Identifier
|
||||||
|
};
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsStudentExcluded(
|
||||||
|
int teamId,
|
||||||
|
int timeSlotIndex,
|
||||||
|
int studentId,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents)
|
||||||
|
{
|
||||||
|
var key = (teamId, timeSlotIndex, studentId);
|
||||||
|
return excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
|
||||||
|
namespace Core.Calculation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Post-processing utilities for team scheduler solutions.
|
||||||
|
/// </summary>
|
||||||
|
public static class TeamSchedulerPostProcessor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extends teams to adjacent time slots (both forward and backward).
|
||||||
|
/// Teams marked as extended will appear in consecutive time slots.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="solution">The scheduler solution to modify</param>
|
||||||
|
/// <param name="extendedTeams">Teams that should be extended to adjacent slots</param>
|
||||||
|
/// <param name="allStudents">All available students for overlap calculations</param>
|
||||||
|
/// <param name="getTeamsWithoutExcludedStudents">Function to filter teams for overlap calculation</param>
|
||||||
|
public static void ExtendTeamsInSolution(
|
||||||
|
TeamSchedulerSolution solution,
|
||||||
|
IEnumerable<Team> extendedTeams,
|
||||||
|
Student[] allStudents,
|
||||||
|
Func<Team[], int, Team[]> getTeamsWithoutExcludedStudents)
|
||||||
|
{
|
||||||
|
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 and extend both forward and backward
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Extend forward: add to next time slot (if exists)
|
||||||
|
if (slotIndex + 1 < solution.TimeSlots.Length)
|
||||||
|
{
|
||||||
|
var nextSlot = solution.TimeSlots[slotIndex + 1];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nextSlot.Teams = nextSlotTeamsList.ToArray();
|
||||||
|
var nextSlotIndex = slotIndex + 1;
|
||||||
|
var nextSlotTeamsForOverlap = getTeamsWithoutExcludedStudents(nextSlot.Teams, nextSlotIndex);
|
||||||
|
nextSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(nextSlotTeamsForOverlap);
|
||||||
|
nextSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(nextSlotTeamsForOverlap, allStudents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend backward: add to previous time slot (if exists)
|
||||||
|
if (slotIndex > 0)
|
||||||
|
{
|
||||||
|
var previousSlot = solution.TimeSlots[slotIndex - 1];
|
||||||
|
var previousSlotTeamsList = previousSlot.Teams.ToList();
|
||||||
|
var previousSlotTeamIds = previousSlotTeamsList.Select(t => t.Id).ToHashSet();
|
||||||
|
|
||||||
|
foreach (var team in teamsToExtend)
|
||||||
|
{
|
||||||
|
if (!previousSlotTeamIds.Contains(team.Id))
|
||||||
|
{
|
||||||
|
previousSlotTeamsList.Add(team);
|
||||||
|
previousSlotTeamIds.Add(team.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
previousSlot.Teams = previousSlotTeamsList.ToArray();
|
||||||
|
var previousSlotIndex = slotIndex - 1;
|
||||||
|
var previousSlotTeamsForOverlap = getTeamsWithoutExcludedStudents(previousSlot.Teams, previousSlotIndex);
|
||||||
|
previousSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(previousSlotTeamsForOverlap);
|
||||||
|
previousSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(previousSlotTeamsForOverlap, allStudents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,5 +12,37 @@ public class PartialTeam : Team
|
|||||||
var omittedStudents = OmittedStudents.Union(Students.Where(studentsToOmit.Contains)).Distinct().ToList();
|
var omittedStudents = OmittedStudents.Union(Students.Where(studentsToOmit.Contains)).Distinct().ToList();
|
||||||
return new PartialTeam{Identifier = Identifier, Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents };
|
return new PartialTeam{Identifier = Identifier, Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates PartialTeam instances from teams with excluded students.
|
||||||
|
/// Aggregates exclusions across all time slots for each team.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="teams">The teams to process</param>
|
||||||
|
/// <param name="excludedStudents">Dictionary of excluded students: key is (teamId, timeSlotIndex, studentId), value is true if excluded</param>
|
||||||
|
/// <returns>Array of teams, with PartialTeam instances for teams with exclusions</returns>
|
||||||
|
public static Team[] CreatePartialTeamsFromExclusions(
|
||||||
|
IEnumerable<Team> teams,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents)
|
||||||
|
{
|
||||||
|
return teams.Select(team =>
|
||||||
|
{
|
||||||
|
// Find all students excluded for this team across all time slots
|
||||||
|
var excludedStudentIds = excludedStudents.Keys
|
||||||
|
.Where(k => k.teamId == team.Id && excludedStudents[k])
|
||||||
|
.Select(k => k.studentId)
|
||||||
|
.Distinct()
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
if (excludedStudentIds.Count == 0)
|
||||||
|
return team;
|
||||||
|
|
||||||
|
var excludedStudentsList = team.Students.Where(s => excludedStudentIds.Contains(s.Id)).ToList();
|
||||||
|
if (excludedStudentsList.Count == 0)
|
||||||
|
return team;
|
||||||
|
|
||||||
|
// Create PartialTeam with excluded students
|
||||||
|
return team.CloneWithOmittedStudents(excludedStudentsList);
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
|
||||||
|
namespace Core.Utility;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for filtering teams based on various criteria.
|
||||||
|
/// </summary>
|
||||||
|
public static class TeamFilterExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds teams that are regional events to the collection.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<Team> AddRegionals(this IEnumerable<Team> currentTeams, IEnumerable<Team> allTeams)
|
||||||
|
{
|
||||||
|
return allTeams.Where(e => e.Event.RegionalEvent).Concat(currentTeams).Distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds teams with high level of effort (>= 3) to the collection.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<Team> AddHighLevelOfEffort(this IEnumerable<Team> currentTeams, IEnumerable<Team> allTeams)
|
||||||
|
{
|
||||||
|
return allTeams.Where(e => e.Event.LevelOfEffort >= 3).Concat(currentTeams).Distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes individual event teams from the collection.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<Team> RemoveIndividual(this IEnumerable<Team> teams)
|
||||||
|
{
|
||||||
|
return teams.Where(t => t.Event.EventFormat != EventFormat.Individual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes teams with low level of effort (<= 1) from the collection.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<Team> RemoveLowLevelOfEffort(this IEnumerable<Team> teams)
|
||||||
|
{
|
||||||
|
return teams.Where(t => t.Event.LevelOfEffort > 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inverts the selection - returns all teams not in the current selection.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<Team> Invert(this IEnumerable<Team> currentTeams, IEnumerable<Team> allTeams)
|
||||||
|
{
|
||||||
|
var currentTeamIds = currentTeams.Select(t => t.Id).ToHashSet();
|
||||||
|
return allTeams.Where(t => !currentTeamIds.Contains(t.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,21 @@
|
|||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@using System.Text
|
@using System.Text
|
||||||
@using Core.Calculation
|
@using Core.Calculation
|
||||||
|
@using Core.Models
|
||||||
|
@using Core.Utility
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
@using WebApp.Components.Shared.Components
|
@using WebApp.Components.Shared.Components
|
||||||
@using WebApp.Components.Features.MeetingSchedule
|
@using WebApp.Components.Features.MeetingSchedule
|
||||||
@using Core.Utility
|
@using WebApp.Models
|
||||||
|
@using WebApp.Services
|
||||||
@inject IConfiguration Configuration
|
@inject IConfiguration Configuration
|
||||||
@inject AppDbContext Context
|
|
||||||
@inject ClipboardService ClipboardService
|
@inject ClipboardService ClipboardService
|
||||||
@inject LocalStorageService LocalStorage
|
|
||||||
@inject IDialogService DialogService
|
@inject IDialogService DialogService
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
@inject IMeetingScheduleStateService StateService
|
||||||
|
@inject IMeetingScheduleDataService DataService
|
||||||
|
@inject IMeetingScheduleClipboardService ClipboardFormatService
|
||||||
|
@inject LocalStorageService LocalStorage
|
||||||
|
|
||||||
<PageHeader Title="@($"{Configuration["ChapterSettings:Shortname"]} TSA Schedule {Configuration["ChapterSettings:CompetitionYear"]}")">
|
<PageHeader Title="@($"{Configuration["ChapterSettings:Shortname"]} TSA Schedule {Configuration["ChapterSettings:CompetitionYear"]}")">
|
||||||
<ActionButtons>
|
<ActionButtons>
|
||||||
@@ -180,37 +185,31 @@
|
|||||||
|
|
||||||
private void AddRegionals()
|
private void AddRegionals()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams = _scheduledTeams.AddRegionals(_teams);
|
||||||
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddHighLevelOfEffort()
|
private void AddHighLevelOfEffort()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams = _scheduledTeams.AddHighLevelOfEffort(_teams);
|
||||||
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveIndividual()
|
private void RemoveIndividual()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams = _scheduledTeams.RemoveIndividual();
|
||||||
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveLowLevelOfEffort()
|
private void RemoveLowLevelOfEffort()
|
||||||
{
|
{
|
||||||
_scheduledTeams
|
_scheduledTeams = _scheduledTeams.RemoveLowLevelOfEffort();
|
||||||
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Invert()
|
private void Invert()
|
||||||
{
|
{
|
||||||
var rt = _scheduledTeams.ToArray();
|
_scheduledTeams = _scheduledTeams.Invert(_teams);
|
||||||
_scheduledTeams
|
|
||||||
= _teams.Where(t => !rt.Contains(t));
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,32 +272,15 @@
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
_teams
|
_teams = await DataService.LoadTeamsAsync();
|
||||||
= await Context.Teams
|
_students = await DataService.LoadStudentsAsync();
|
||||||
.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
|
// Load saved selections from localStorage
|
||||||
await LoadScheduledTeams();
|
_scheduledTeams = await StateService.LoadScheduledTeamsAsync(_teams);
|
||||||
await LoadAbsentStudents();
|
_absentStudents = await StateService.LoadAbsentStudentsAsync(_students);
|
||||||
await LoadTimeSlotCount();
|
_parameters.TimeSlots = await StateService.LoadTimeSlotCountAsync(2);
|
||||||
await LoadExtendedTeams();
|
_extendedTeams = await StateService.LoadExtendedTeamsAsync(_teams);
|
||||||
await LoadExcludedStudents();
|
_excludedStudents = await StateService.LoadExcludedStudentsAsync();
|
||||||
|
|
||||||
// Initialize last saved state from loaded values
|
// Initialize last saved state from loaded values
|
||||||
_lastSavedState = await MeetingScheduleState.FromLocalStorage(LocalStorage, _teams, _students);
|
_lastSavedState = await MeetingScheduleState.FromLocalStorage(LocalStorage, _teams, _students);
|
||||||
@@ -314,84 +296,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 SaveExcludedStudents()
|
|
||||||
{
|
|
||||||
var exclusions = _excludedStudents.Keys
|
|
||||||
.Where(k => _excludedStudents[k])
|
|
||||||
.Select(k => new ExcludedStudent(k.teamId, k.timeSlotIndex, k.studentId))
|
|
||||||
.ToArray();
|
|
||||||
await LocalStorage.SetJsonAsync("MeetingSchedule_ExcludedStudents", exclusions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LoadExcludedStudents()
|
|
||||||
{
|
|
||||||
var exclusions = await LocalStorage.GetJsonAsync<ExcludedStudent[]>("MeetingSchedule_ExcludedStudents");
|
|
||||||
if (exclusions != null && exclusions.Length > 0)
|
|
||||||
{
|
|
||||||
_excludedStudents = exclusions.ToDictionary(
|
|
||||||
e => (e.TeamId, e.TimeSlotIndex, e.StudentId),
|
|
||||||
_ => true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetTimeSlotIndex(string timeSlotName)
|
private int GetTimeSlotIndex(string timeSlotName)
|
||||||
{
|
{
|
||||||
@@ -425,40 +329,12 @@
|
|||||||
return _excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded;
|
return _excludedStudents.TryGetValue(key, out var isExcluded) && isExcluded;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates teams with excluded students and absent students filtered out for overlap calculation.
|
|
||||||
/// </summary>
|
|
||||||
private Team[] GetTeamsWithoutExcludedStudents(Team[] teams, int timeSlotIndex)
|
private Team[] GetTeamsWithoutExcludedStudents(Team[] teams, int timeSlotIndex)
|
||||||
{
|
{
|
||||||
var absentStudentIds = _absentStudents.Select(s => s.Id).ToHashSet();
|
var absentStudentIds = _absentStudents.Select(s => s.Id).ToHashSet();
|
||||||
|
return OverlapCalculationHelper.GetTeamsWithoutExcludedStudents(teams, timeSlotIndex, _excludedStudents, absentStudentIds);
|
||||||
return teams.Select(team =>
|
|
||||||
{
|
|
||||||
// Find excluded students for this team in this time slot
|
|
||||||
// Also exclude absent students from overlap calculations
|
|
||||||
// More efficient: iterate through team students and check exclusions
|
|
||||||
var includedStudents = team.Students
|
|
||||||
.Where(s => !IsStudentExcluded(team.Id, timeSlotIndex, s.Id) && !absentStudentIds.Contains(s.Id))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// If no students are excluded, return original team
|
|
||||||
if (includedStudents.Count == team.Students.Count)
|
|
||||||
return team;
|
|
||||||
|
|
||||||
// Create a temporary team with excluded and absent students removed
|
|
||||||
return new Team
|
|
||||||
{
|
|
||||||
Id = team.Id,
|
|
||||||
Event = team.Event,
|
|
||||||
Students = includedStudents,
|
|
||||||
Captain = team.Captain,
|
|
||||||
Identifier = team.Identifier
|
|
||||||
};
|
|
||||||
}).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ExcludedStudent(int TeamId, int TimeSlotIndex, int StudentId);
|
|
||||||
|
|
||||||
private bool IsDirty()
|
private bool IsDirty()
|
||||||
{
|
{
|
||||||
if (_lastSavedState == null)
|
if (_lastSavedState == null)
|
||||||
@@ -507,26 +383,7 @@
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
// Create PartialTeam instances for teams with excluded students
|
// Create PartialTeam instances for teams with excluded students
|
||||||
// Aggregate exclusions across all time slots (since we don't know assignment yet)
|
var teamsForScheduling = PartialTeam.CreatePartialTeamsFromExclusions(_scheduledTeams, _excludedStudents);
|
||||||
var teamsForScheduling = _scheduledTeams.Select(team =>
|
|
||||||
{
|
|
||||||
// Find all students excluded for this team across all time slots
|
|
||||||
var excludedStudentIds = _excludedStudents.Keys
|
|
||||||
.Where(k => k.teamId == team.Id && _excludedStudents[k])
|
|
||||||
.Select(k => k.studentId)
|
|
||||||
.Distinct()
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
if (excludedStudentIds.Count == 0)
|
|
||||||
return team;
|
|
||||||
|
|
||||||
var excludedStudents = team.Students.Where(s => excludedStudentIds.Contains(s.Id)).ToList();
|
|
||||||
if (excludedStudents.Count == 0)
|
|
||||||
return team;
|
|
||||||
|
|
||||||
// Create PartialTeam with excluded students
|
|
||||||
return team.CloneWithOmittedStudents(excludedStudents);
|
|
||||||
}).ToArray();
|
|
||||||
|
|
||||||
var teamScheduler = new TeamScheduler(teamsForScheduling, _parameters.TimeSlots, availableStudents);
|
var teamScheduler = new TeamScheduler(teamsForScheduling, _parameters.TimeSlots, availableStudents);
|
||||||
_solution = teamScheduler.Solve();
|
_solution = teamScheduler.Solve();
|
||||||
@@ -564,7 +421,11 @@
|
|||||||
// Post-process: extend teams to next consecutive time slot
|
// Post-process: extend teams to next consecutive time slot
|
||||||
if (_extendedTeams.Any())
|
if (_extendedTeams.Any())
|
||||||
{
|
{
|
||||||
ExtendTeamsInSolution(_solution, _extendedTeams, availableStudents);
|
TeamSchedulerPostProcessor.ExtendTeamsInSolution(
|
||||||
|
_solution,
|
||||||
|
_extendedTeams,
|
||||||
|
availableStudents,
|
||||||
|
GetTeamsWithoutExcludedStudents);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try recommendation strategies in priority order
|
// Try recommendation strategies in priority order
|
||||||
@@ -590,6 +451,13 @@
|
|||||||
_excludedStudents);
|
_excludedStudents);
|
||||||
await currentState.SaveToLocalStorage(LocalStorage);
|
await currentState.SaveToLocalStorage(LocalStorage);
|
||||||
_lastSavedState = currentState;
|
_lastSavedState = currentState;
|
||||||
|
|
||||||
|
// Also save via state service for consistency
|
||||||
|
await StateService.SaveScheduledTeamsAsync(_scheduledTeams);
|
||||||
|
await StateService.SaveAbsentStudentsAsync(_absentStudents);
|
||||||
|
await StateService.SaveTimeSlotCountAsync(_parameters.TimeSlots);
|
||||||
|
await StateService.SaveExtendedTeamsAsync(_extendedTeams);
|
||||||
|
await StateService.SaveExcludedStudentsAsync(_excludedStudents);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
|
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
|
||||||
|
|
||||||
@@ -602,91 +470,18 @@
|
|||||||
_solutionData.ReloadServerData();
|
_solutionData.ReloadServerData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move team extension logic into Core.Calculation.TeamScheduler to handle extended teams
|
|
||||||
// as part of the constraint programming model rather than post-processing
|
|
||||||
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 and extend both forward and backward
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Extend forward: add to next time slot (if exists)
|
|
||||||
if (slotIndex + 1 < solution.TimeSlots.Length)
|
|
||||||
{
|
|
||||||
var nextSlot = solution.TimeSlots[slotIndex + 1];
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextSlot.Teams = nextSlotTeamsList.ToArray();
|
|
||||||
var nextSlotIndex = slotIndex + 1;
|
|
||||||
var nextSlotTeamsForOverlap = GetTeamsWithoutExcludedStudents(nextSlot.Teams, nextSlotIndex);
|
|
||||||
nextSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(nextSlotTeamsForOverlap);
|
|
||||||
// Use teams without excluded students so students excluded from all teams appear as unscheduled
|
|
||||||
nextSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(nextSlotTeamsForOverlap, allStudents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extend backward: add to previous time slot (if exists)
|
|
||||||
if (slotIndex > 0)
|
|
||||||
{
|
|
||||||
var previousSlot = solution.TimeSlots[slotIndex - 1];
|
|
||||||
var previousSlotTeamsList = previousSlot.Teams.ToList();
|
|
||||||
var previousSlotTeamIds = previousSlotTeamsList.Select(t => t.Id).ToHashSet();
|
|
||||||
|
|
||||||
foreach (var team in teamsToExtend)
|
|
||||||
{
|
|
||||||
if (!previousSlotTeamIds.Contains(team.Id))
|
|
||||||
{
|
|
||||||
previousSlotTeamsList.Add(team);
|
|
||||||
previousSlotTeamIds.Add(team.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
previousSlot.Teams = previousSlotTeamsList.ToArray();
|
|
||||||
var previousSlotIndex = slotIndex - 1;
|
|
||||||
var previousSlotTeamsForOverlap = GetTeamsWithoutExcludedStudents(previousSlot.Teams, previousSlotIndex);
|
|
||||||
previousSlot.StudentOverlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(previousSlotTeamsForOverlap);
|
|
||||||
// Use teams without excluded students so students excluded from all teams appear as unscheduled
|
|
||||||
previousSlot.UnscheduledStudents = TeamSchedulerSolution.GetStudentsNotInTimSlot(previousSlotTeamsForOverlap, allStudents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task CopyToClipboard()
|
async Task CopyToClipboard()
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
|
||||||
foreach (var timeslot in _solution.TimeSlots)
|
|
||||||
{
|
|
||||||
AppendScheduledTeams(sb, timeslot);
|
|
||||||
AppendUnscheduledStudents(sb, timeslot);
|
|
||||||
sb.Append(Environment.NewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ClipboardService.WriteTextAsync(sb.ToString());
|
var text = ClipboardFormatService.FormatScheduleForClipboard(
|
||||||
|
_solution,
|
||||||
|
_teams,
|
||||||
|
_absentStudents,
|
||||||
|
_excludedStudents,
|
||||||
|
GetTimeSlotIndex);
|
||||||
|
await ClipboardService.WriteTextAsync(text);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -717,220 +512,6 @@
|
|||||||
// Note: Success message is already shown in the dialog, no need to show another here
|
// Note: Success message is already shown in the dialog, no need to show another here
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendScheduledTeams(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
|
||||||
{
|
|
||||||
var timeSlotIndex = GetTimeSlotIndex(timeslot.Name);
|
|
||||||
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, timeSlotIndex);
|
|
||||||
sb.Append($"{teamName} - {studentsList}");
|
|
||||||
}
|
|
||||||
sb.Append(Environment.NewLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot, int timeSlotIndex)
|
|
||||||
{
|
|
||||||
// Filter out excluded students for this team and time slot
|
|
||||||
var excludedStudentIds = _excludedStudents.Keys
|
|
||||||
.Where(k => k.teamId == team.Id && k.timeSlotIndex == timeSlotIndex && _excludedStudents[k])
|
|
||||||
.Select(k => k.studentId)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
var includedStudents = team.Students
|
|
||||||
.Where(s => !excludedStudentIds.Contains(s.Id))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// Create a temporary team with only included students for formatting
|
|
||||||
var teamForFormatting = new Team
|
|
||||||
{
|
|
||||||
Id = team.Id,
|
|
||||||
Event = team.Event,
|
|
||||||
Students = includedStudents,
|
|
||||||
Captain = team.Captain,
|
|
||||||
Identifier = team.Identifier
|
|
||||||
};
|
|
||||||
|
|
||||||
return TeamStudentNameFormatter.FormatStudentList(
|
|
||||||
teamForFormatting,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,6 +101,6 @@
|
|||||||
|
|
||||||
private void Cancel()
|
private void Cancel()
|
||||||
{
|
{
|
||||||
MudDialog.Cancel();
|
MudDialog.Close(DialogResult.Cancel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
Disabled="@_isLoading">
|
Disabled="@_isLoading">
|
||||||
Edit
|
Edit
|
||||||
</MudButton>
|
</MudButton>
|
||||||
<MudButton OnClick="Cancel">Close</MudButton>
|
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||||
}
|
}
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
@@ -242,14 +242,12 @@
|
|||||||
if (_isDisposed) return;
|
if (_isDisposed) return;
|
||||||
if (_isEditMode)
|
if (_isEditMode)
|
||||||
{
|
{
|
||||||
// If in edit mode, cancel goes back to view mode
|
// If in edit mode, cancel closes the dialog (discarding changes)
|
||||||
_isEditMode = false;
|
MudDialog.Close(DialogResult.Cancel());
|
||||||
// Reload the note to discard changes
|
|
||||||
_ = LoadPageNote();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
MudDialog.Cancel();
|
MudDialog.Close(DialogResult.Cancel());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
|
||||||
|
namespace WebApp.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the state of the meeting schedule for dirty/clean tracking and persistence.
|
||||||
|
/// </summary>
|
||||||
|
public 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(
|
||||||
|
WebApp.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(WebApp.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record ExcludedStudent(int TeamId, int TimeSlotIndex, int StudentId);
|
||||||
|
}
|
||||||
@@ -197,6 +197,9 @@ builder.Services.AddScoped<WebApp.Services.INotesService, WebApp.Services.NotesS
|
|||||||
builder.Services.AddScoped<WebApp.Services.ITeamMeetingHistoryService, WebApp.Services.TeamMeetingHistoryService>();
|
builder.Services.AddScoped<WebApp.Services.ITeamMeetingHistoryService, WebApp.Services.TeamMeetingHistoryService>();
|
||||||
builder.Services.AddScoped<Core.Services.INoteNamingService, Core.Services.NoteNamingService>();
|
builder.Services.AddScoped<Core.Services.INoteNamingService, Core.Services.NoteNamingService>();
|
||||||
builder.Services.AddScoped<WebApp.Services.MarkdownTablePasteService>();
|
builder.Services.AddScoped<WebApp.Services.MarkdownTablePasteService>();
|
||||||
|
builder.Services.AddScoped<WebApp.Services.IMeetingScheduleStateService, WebApp.Services.MeetingScheduleStateService>();
|
||||||
|
builder.Services.AddScoped<WebApp.Services.IMeetingScheduleClipboardService, WebApp.Services.MeetingScheduleClipboardService>();
|
||||||
|
builder.Services.AddScoped<WebApp.Services.IMeetingScheduleDataService, WebApp.Services.MeetingScheduleDataService>();
|
||||||
|
|
||||||
// State container for maintaining state per user connection (Blazor Server)
|
// State container for maintaining state per user connection (Blazor Server)
|
||||||
builder.Services.AddScoped<StateContainer>();
|
builder.Services.AddScoped<StateContainer>();
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Core.Calculation;
|
||||||
|
using Core.Entities;
|
||||||
|
using Core.Utility;
|
||||||
|
|
||||||
|
namespace WebApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for formatting meeting schedule data for clipboard operations.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMeetingScheduleClipboardService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Formats the entire schedule solution as text for clipboard.
|
||||||
|
/// </summary>
|
||||||
|
string FormatScheduleForClipboard(
|
||||||
|
TeamSchedulerSolution solution,
|
||||||
|
Team[] allTeams,
|
||||||
|
IEnumerable<Student> absentStudents,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents,
|
||||||
|
Func<string, int> getTimeSlotIndex);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace WebApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for loading meeting schedule data from the database.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMeetingScheduleDataService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all teams with their events and students.
|
||||||
|
/// </summary>
|
||||||
|
Task<Team[]> LoadTeamsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all students with their teams, event rankings, and related data.
|
||||||
|
/// </summary>
|
||||||
|
Task<Student[]> LoadStudentsAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
|
||||||
|
namespace WebApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for managing meeting schedule state persistence in localStorage.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMeetingScheduleStateService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Loads scheduled teams from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<Team>> LoadScheduledTeamsAsync(Team[] allTeams);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves scheduled teams to localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveScheduledTeamsAsync(IEnumerable<Team> scheduledTeams);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads absent students from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<Student>> LoadAbsentStudentsAsync(Student[] allStudents);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves absent students to localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveAbsentStudentsAsync(IEnumerable<Student> absentStudents);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads time slot count from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task<int> LoadTimeSlotCountAsync(int defaultValue = 2);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves time slot count to localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveTimeSlotCountAsync(int timeSlotCount);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads extended teams from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task<IEnumerable<Team>> LoadExtendedTeamsAsync(Team[] allTeams);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves extended teams to localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveExtendedTeamsAsync(IEnumerable<Team> extendedTeams);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads excluded students from localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task<Dictionary<(int teamId, int timeSlotIndex, int studentId), bool>> LoadExcludedStudentsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Saves excluded students to localStorage.
|
||||||
|
/// </summary>
|
||||||
|
Task SaveExcludedStudentsAsync(Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents);
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using System.Text;
|
||||||
|
using Core.Calculation;
|
||||||
|
using Core.Entities;
|
||||||
|
using Core.Utility;
|
||||||
|
|
||||||
|
namespace WebApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for formatting meeting schedule data for clipboard operations.
|
||||||
|
/// </summary>
|
||||||
|
public class MeetingScheduleClipboardService : IMeetingScheduleClipboardService
|
||||||
|
{
|
||||||
|
public string FormatScheduleForClipboard(
|
||||||
|
TeamSchedulerSolution solution,
|
||||||
|
Team[] allTeams,
|
||||||
|
IEnumerable<Student> absentStudents,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents,
|
||||||
|
Func<string, int> getTimeSlotIndex)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
foreach (var timeslot in solution.TimeSlots)
|
||||||
|
{
|
||||||
|
AppendScheduledTeams(sb, timeslot, allTeams, absentStudents, excludedStudents, getTimeSlotIndex);
|
||||||
|
AppendUnscheduledStudents(sb, timeslot, solution, absentStudents);
|
||||||
|
sb.Append(Environment.NewLine);
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendScheduledTeams(
|
||||||
|
StringBuilder sb,
|
||||||
|
TeamScheduleTimeSlot timeslot,
|
||||||
|
Team[] allTeams,
|
||||||
|
IEnumerable<Student> absentStudents,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents,
|
||||||
|
Func<string, int> getTimeSlotIndex)
|
||||||
|
{
|
||||||
|
var timeSlotIndex = getTimeSlotIndex(timeslot.Name);
|
||||||
|
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, timeSlotIndex, absentStudents, excludedStudents);
|
||||||
|
sb.Append($"{teamName} - {studentsList}");
|
||||||
|
}
|
||||||
|
sb.Append(Environment.NewLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatStudentList(
|
||||||
|
Team team,
|
||||||
|
TeamScheduleTimeSlot timeslot,
|
||||||
|
int timeSlotIndex,
|
||||||
|
IEnumerable<Student> absentStudents,
|
||||||
|
Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents)
|
||||||
|
{
|
||||||
|
// Filter out excluded students for this team and time slot
|
||||||
|
var excludedStudentIds = excludedStudents.Keys
|
||||||
|
.Where(k => k.teamId == team.Id && k.timeSlotIndex == timeSlotIndex && excludedStudents[k])
|
||||||
|
.Select(k => k.studentId)
|
||||||
|
.ToHashSet();
|
||||||
|
|
||||||
|
var includedStudents = team.Students
|
||||||
|
.Where(s => !excludedStudentIds.Contains(s.Id))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Create a temporary team with only included students for formatting
|
||||||
|
var teamForFormatting = new Team
|
||||||
|
{
|
||||||
|
Id = team.Id,
|
||||||
|
Event = team.Event,
|
||||||
|
Students = includedStudents,
|
||||||
|
Captain = team.Captain,
|
||||||
|
Identifier = team.Identifier
|
||||||
|
};
|
||||||
|
|
||||||
|
return TeamStudentNameFormatter.FormatStudentList(
|
||||||
|
teamForFormatting,
|
||||||
|
new TeamStudentNameFormatter.FormatOptions
|
||||||
|
{
|
||||||
|
Ordering = TeamStudentNameFormatter.OrderingStyle.CaptainFirst,
|
||||||
|
MarkOverlaps = true,
|
||||||
|
HasOverlaps = timeslot.StudentHasOverlaps,
|
||||||
|
MarkAbsent = true,
|
||||||
|
AbsentStudents = absentStudents.ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendUnscheduledStudents(
|
||||||
|
StringBuilder sb,
|
||||||
|
TeamScheduleTimeSlot timeslot,
|
||||||
|
TeamSchedulerSolution solution,
|
||||||
|
IEnumerable<Student> absentStudents)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
using Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace WebApp.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for loading meeting schedule data from the database.
|
||||||
|
/// </summary>
|
||||||
|
public class MeetingScheduleDataService : IMeetingScheduleDataService
|
||||||
|
{
|
||||||
|
private readonly AppDbContext _context;
|
||||||
|
|
||||||
|
public MeetingScheduleDataService(AppDbContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Team[]> LoadTeamsAsync()
|
||||||
|
{
|
||||||
|
return await _context.Teams
|
||||||
|
.AsNoTracking()
|
||||||
|
.Include(e => e.Event)
|
||||||
|
.Include(e => e.Students)
|
||||||
|
.OrderBy(e => e.Event.Name)
|
||||||
|
.ThenBy(e => e.Identifier)
|
||||||
|
.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Student[]> LoadStudentsAsync()
|
||||||
|
{
|
||||||
|
return 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
using Core.Entities;
|
||||||
|
using WebApp.Models;
|
||||||
|
|
||||||
|
namespace WebApp.Services;
|
||||||
|
|
||||||
|
internal record ExcludedStudent(int TeamId, int TimeSlotIndex, int StudentId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for managing meeting schedule state persistence in localStorage.
|
||||||
|
/// </summary>
|
||||||
|
public class MeetingScheduleStateService : IMeetingScheduleStateService
|
||||||
|
{
|
||||||
|
private readonly LocalStorageService _localStorage;
|
||||||
|
private const string ScheduledTeamsKey = "MeetingSchedule_ScheduledTeams";
|
||||||
|
private const string AbsentStudentsKey = "MeetingSchedule_AbsentStudents";
|
||||||
|
private const string TimeSlotCountKey = "MeetingSchedule_TimeSlotCount";
|
||||||
|
private const string ExtendedTeamsKey = "MeetingSchedule_ExtendedTeams";
|
||||||
|
private const string ExcludedStudentsKey = "MeetingSchedule_ExcludedStudents";
|
||||||
|
|
||||||
|
public MeetingScheduleStateService(LocalStorageService localStorage)
|
||||||
|
{
|
||||||
|
_localStorage = localStorage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Team>> LoadScheduledTeamsAsync(Team[] allTeams)
|
||||||
|
{
|
||||||
|
var teamIds = await _localStorage.GetIntArrayAsync(ScheduledTeamsKey);
|
||||||
|
if (teamIds.Length > 0)
|
||||||
|
{
|
||||||
|
return allTeams.Where(t => teamIds.Contains(t.Id)).ToArray();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveScheduledTeamsAsync(IEnumerable<Team> scheduledTeams)
|
||||||
|
{
|
||||||
|
var teamIds = scheduledTeams.Select(t => t.Id).ToArray();
|
||||||
|
await _localStorage.SetIntArrayAsync(ScheduledTeamsKey, teamIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Student>> LoadAbsentStudentsAsync(Student[] allStudents)
|
||||||
|
{
|
||||||
|
var studentIds = await _localStorage.GetIntArrayAsync(AbsentStudentsKey);
|
||||||
|
if (studentIds.Length > 0)
|
||||||
|
{
|
||||||
|
return allStudents.Where(s => studentIds.Contains(s.Id)).ToArray();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveAbsentStudentsAsync(IEnumerable<Student> absentStudents)
|
||||||
|
{
|
||||||
|
var studentIds = absentStudents.Select(s => s.Id).ToArray();
|
||||||
|
await _localStorage.SetIntArrayAsync(AbsentStudentsKey, studentIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> LoadTimeSlotCountAsync(int defaultValue = 2)
|
||||||
|
{
|
||||||
|
var timeSlots = await _localStorage.GetIntAsync(TimeSlotCountKey, defaultValue);
|
||||||
|
return timeSlots > 0 ? timeSlots : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveTimeSlotCountAsync(int timeSlotCount)
|
||||||
|
{
|
||||||
|
await _localStorage.SetIntAsync(TimeSlotCountKey, timeSlotCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IEnumerable<Team>> LoadExtendedTeamsAsync(Team[] allTeams)
|
||||||
|
{
|
||||||
|
var teamIds = await _localStorage.GetIntArrayAsync(ExtendedTeamsKey);
|
||||||
|
if (teamIds.Length > 0)
|
||||||
|
{
|
||||||
|
return allTeams.Where(t => teamIds.Contains(t.Id)).ToArray();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveExtendedTeamsAsync(IEnumerable<Team> extendedTeams)
|
||||||
|
{
|
||||||
|
var teamIds = extendedTeams.Select(t => t.Id).ToArray();
|
||||||
|
await _localStorage.SetIntArrayAsync(ExtendedTeamsKey, teamIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Dictionary<(int teamId, int timeSlotIndex, int studentId), bool>> LoadExcludedStudentsAsync()
|
||||||
|
{
|
||||||
|
var exclusions = await _localStorage.GetJsonAsync<ExcludedStudent[]>(ExcludedStudentsKey);
|
||||||
|
if (exclusions != null && exclusions.Length > 0)
|
||||||
|
{
|
||||||
|
return exclusions.ToDictionary(
|
||||||
|
e => (e.TeamId, e.TimeSlotIndex, e.StudentId),
|
||||||
|
_ => true);
|
||||||
|
}
|
||||||
|
return new Dictionary<(int teamId, int timeSlotIndex, int studentId), bool>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveExcludedStudentsAsync(Dictionary<(int teamId, int timeSlotIndex, int studentId), bool> excludedStudents)
|
||||||
|
{
|
||||||
|
var exclusions = excludedStudents.Keys
|
||||||
|
.Where(k => excludedStudents[k])
|
||||||
|
.Select(k => new ExcludedStudent(k.teamId, k.timeSlotIndex, k.studentId))
|
||||||
|
.ToArray();
|
||||||
|
await _localStorage.SetJsonAsync(ExcludedStudentsKey, exclusions);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user