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:
2026-01-19 23:02:55 -05:00
parent 649a0061cf
commit ddb743847d
15 changed files with 766 additions and 467 deletions
@@ -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);
}
}
}
}
+32
View File
@@ -12,5 +12,37 @@ public class PartialTeam : Team
var omittedStudents = OmittedStudents.Union(Students.Where(studentsToOmit.Contains)).Distinct().ToList();
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();
}
}
+50
View File
@@ -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));
}
}