Refactor TeamScheduler, easier to read and maintain
This commit is contained in:
@@ -2,15 +2,38 @@
|
|||||||
|
|
||||||
namespace Core.Calculation;
|
namespace Core.Calculation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single time slot in the team meeting schedule.
|
||||||
|
/// </summary>
|
||||||
public class TeamScheduleTimeSlot
|
public class TeamScheduleTimeSlot
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the name of this time slot.
|
||||||
|
/// </summary>
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public Team[] Teams;
|
|
||||||
public Student[] UnscheduledStudents;
|
|
||||||
public IEnumerable<Tuple<Student, IEnumerable<Team>>> StudentOverlaps;
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the teams scheduled in this time slot.
|
||||||
|
/// </summary>
|
||||||
|
public Team[] Teams;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the students who are not scheduled in any team during this time slot.
|
||||||
|
/// </summary>
|
||||||
|
public Student[] UnscheduledStudents;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the students who have overlapping team meetings in this time slot.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<(Student student, IEnumerable<Team> teams)> StudentOverlaps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a student has overlapping team meetings in this time slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="student">The student to check</param>
|
||||||
|
/// <returns>True if the student has overlaps, otherwise false</returns>
|
||||||
public bool StudentHasOverlaps(Student student)
|
public bool StudentHasOverlaps(Student student)
|
||||||
{
|
{
|
||||||
return StudentOverlaps.FirstOrDefault(o => o.Item1.Equals(student)) != null;
|
return StudentOverlaps.Any(o => o.student.Equals(student));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,69 +3,110 @@ using Core.Entities;
|
|||||||
using Google.OrTools.Sat;
|
using Google.OrTools.Sat;
|
||||||
|
|
||||||
namespace Core.Calculation;
|
namespace Core.Calculation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves the team meeting scheduling problem using constraint programming.
|
||||||
|
/// Assigns teams to time slots while minimizing student schedule conflicts.
|
||||||
|
/// </summary>
|
||||||
public class TeamScheduler
|
public class TeamScheduler
|
||||||
{
|
{
|
||||||
private readonly IList<Student> _studentObjects;
|
private readonly IList<Student> _studentObjects;
|
||||||
private readonly IList<Team> _teamObjects;
|
private readonly IList<Team> _teamObjects;
|
||||||
|
private readonly double _maxSolveTimeSeconds;
|
||||||
|
|
||||||
private readonly int[] _students;
|
private readonly int[] _students;
|
||||||
private readonly int[] _teams;
|
private readonly int[] _teams;
|
||||||
private readonly int[] _timeSlots;
|
private readonly int[] _timeSlots;
|
||||||
|
|
||||||
private readonly List<Tuple<int,int>> _scheduleSeparateTeams = [];
|
private readonly List<(int team1, int team2)> _scheduleSeparateTeams = [];
|
||||||
|
|
||||||
public TeamScheduler(IEnumerable<Team> teams, int numTimeSlots, IEnumerable<Student> allStudents)
|
/// <summary>
|
||||||
|
/// Creates a new team scheduler instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="teams">The teams to schedule (must not be null or empty)</param>
|
||||||
|
/// <param name="numTimeSlots">The number of available time slots (must be positive)</param>
|
||||||
|
/// <param name="allStudents">All students participating in teams (must not be null or empty)</param>
|
||||||
|
/// <param name="maxSolveTimeSeconds">Maximum solver time in seconds (default: 10.0)</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when teams or allStudents is null</exception>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when collections are empty or numTimeSlots is invalid</exception>
|
||||||
|
public TeamScheduler(IEnumerable<Team> teams, int numTimeSlots, IEnumerable<Student> allStudents, double maxSolveTimeSeconds = 10.0)
|
||||||
{
|
{
|
||||||
|
if (teams == null)
|
||||||
|
throw new ArgumentNullException(nameof(teams));
|
||||||
|
if (allStudents == null)
|
||||||
|
throw new ArgumentNullException(nameof(allStudents));
|
||||||
|
if (numTimeSlots <= 0)
|
||||||
|
throw new ArgumentException("Number of time slots must be positive", nameof(numTimeSlots));
|
||||||
|
|
||||||
_teamObjects = teams.ToArray();
|
_teamObjects = teams.ToArray();
|
||||||
//_studentObjects = teams.SelectMany(t => t.Students).Distinct().ToList();
|
_studentObjects = allStudents.ToList();
|
||||||
_studentObjects = allStudents.ToList();
|
_maxSolveTimeSeconds = maxSolveTimeSeconds;
|
||||||
|
|
||||||
|
if (_teamObjects.Count == 0)
|
||||||
|
throw new ArgumentException("Teams collection cannot be empty", nameof(teams));
|
||||||
|
if (_studentObjects.Count == 0)
|
||||||
|
throw new ArgumentException("Students collection cannot be empty", nameof(allStudents));
|
||||||
|
|
||||||
_students = Enumerable.Range(0, _studentObjects.Count).ToArray();
|
_students = Enumerable.Range(0, _studentObjects.Count).ToArray();
|
||||||
_teams = Enumerable.Range(0, _teamObjects.Count).ToArray();
|
_teams = Enumerable.Range(0, _teamObjects.Count).ToArray();
|
||||||
_timeSlots = Enumerable.Range(0, numTimeSlots).ToArray();
|
_timeSlots = Enumerable.Range(0, numTimeSlots).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a constraint requiring two teams to be scheduled in different time slots.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="team1">First team</param>
|
||||||
|
/// <param name="team2">Second team</param>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when either team is not found in the scheduler</exception>
|
||||||
public void ScheduleSeparate(Team team1, Team team2)
|
public void ScheduleSeparate(Team team1, Team team2)
|
||||||
{
|
{
|
||||||
var one = _teamObjects.IndexOf(team1);
|
var one = _teamObjects.IndexOf(team1);
|
||||||
var two = _teamObjects.IndexOf(team2);
|
var two = _teamObjects.IndexOf(team2);
|
||||||
_scheduleSeparateTeams.Add(Tuple.Create(one,two));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TeamScheduler CreateInstance(IEnumerable<Team> teams, int numTimeSlots, IEnumerable<Student> allStudents)
|
if (one == -1)
|
||||||
{
|
throw new ArgumentException($"Team '{team1}' not found in scheduler", nameof(team1));
|
||||||
return new TeamScheduler(teams, numTimeSlots, allStudents);
|
if (two == -1)
|
||||||
|
throw new ArgumentException($"Team '{team2}' not found in scheduler", nameof(team2));
|
||||||
|
|
||||||
|
_scheduleSeparateTeams.Add((one, two));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves the team scheduling problem using constraint programming.
|
||||||
|
/// Minimizes the number of time slots where students have conflicting team meetings.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A solution containing team assignments to time slots and conflict information</returns>
|
||||||
public TeamSchedulerSolution Solve()
|
public TeamSchedulerSolution Solve()
|
||||||
{
|
{
|
||||||
// Model.
|
// Create constraint programming model
|
||||||
var model = new CpModel();
|
var model = new CpModel();
|
||||||
|
|
||||||
// Data
|
// Build membership matrix: m[i,t] = 1 if student i is on team t, else 0
|
||||||
var m = new int[_students.Length,_teams.Length];
|
var m = new int[_students.Length,_teams.Length];
|
||||||
foreach (var i in _students)
|
foreach (var i in _students)
|
||||||
foreach (var t in _teams)
|
foreach (var t in _teams)
|
||||||
m[i, t] = _studentObjects[i].Teams.Contains(_teamObjects[t]) ? 1 : 0;
|
m[i, t] = _studentObjects[i].Teams.Contains(_teamObjects[t]) ? 1 : 0;
|
||||||
|
|
||||||
// Variables.
|
// Decision variables:
|
||||||
// x - 1 if meeting of team t takes place at time slot s, else 0
|
// x[t,s] = 1 if meeting of team t takes place at time slot s, else 0
|
||||||
var x = new IntVar[_teams.Length, _timeSlots.Length];
|
var x = new IntVar[_teams.Length, _timeSlots.Length];
|
||||||
foreach (var t in _teams)
|
foreach (var t in _teams)
|
||||||
foreach (var s in _timeSlots)
|
foreach (var s in _timeSlots)
|
||||||
x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]");
|
x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]");
|
||||||
|
|
||||||
// y - 1 if individual i has meetings at time slot s, 0 otherwise
|
// y[i,s] = 1 if student i has at least one meeting at time slot s, else 0
|
||||||
var y = new IntVar[_students.Length, _timeSlots.Length];
|
var y = new IntVar[_students.Length, _timeSlots.Length];
|
||||||
foreach (var i in _students)
|
foreach (var i in _students)
|
||||||
foreach (var s in _timeSlots)
|
foreach (var s in _timeSlots)
|
||||||
y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]");
|
y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]");
|
||||||
|
|
||||||
// each team meets exactly one time
|
// Constraint: each team meets exactly once
|
||||||
foreach (var t in _teams)
|
foreach (var t in _teams)
|
||||||
model.AddLinearConstraint(LinearExpr.Sum(_timeSlots.Select(s => x[t, s])), 1L, 1L);
|
model.AddLinearConstraint(LinearExpr.Sum(_timeSlots.Select(s => x[t, s])), 1L, 1L);
|
||||||
|
|
||||||
// individual must have at least one team meeting at the given time slot to attend
|
// Constraint: Link y[i,s] to whether student i has meetings at slot s
|
||||||
|
// y[i,s] <= sum over all teams t of (m[i,t] * x[t,s])
|
||||||
|
// This forces y[i,s] to be 1 if student i has at least one team meeting at slot s
|
||||||
foreach (var i in _students)
|
foreach (var i in _students)
|
||||||
foreach (var s in _timeSlots)
|
foreach (var s in _timeSlots)
|
||||||
model.Add(
|
model.Add(
|
||||||
@@ -74,19 +115,21 @@ public class TeamScheduler
|
|||||||
LinearExpr.Sum(_teams.Select(t => m[i, t] * x[t, s])),
|
LinearExpr.Sum(_teams.Select(t => m[i, t] * x[t, s])),
|
||||||
false));
|
false));
|
||||||
|
|
||||||
// maximize number of times individuals meet
|
// Objective: minimize the sum of y[i,s] values (minimize student conflicts)
|
||||||
var indTimeSlotVars = LinearExpr.NewBuilder();
|
var indTimeSlotVars = LinearExpr.NewBuilder();
|
||||||
foreach (var i in _students)
|
foreach (var i in _students)
|
||||||
foreach (var s in _timeSlots)
|
foreach (var s in _timeSlots)
|
||||||
indTimeSlotVars.Add(y[i, s]);
|
indTimeSlotVars.Add(y[i, s]);
|
||||||
model.Minimize(indTimeSlotVars);
|
model.Minimize(indTimeSlotVars);
|
||||||
|
|
||||||
foreach (var ts in _scheduleSeparateTeams)
|
// Constraint: teams marked as "separate" must be in different time slots
|
||||||
|
foreach (var (team1, team2) in _scheduleSeparateTeams)
|
||||||
foreach (var s in _timeSlots)
|
foreach (var s in _timeSlots)
|
||||||
model.Add(x[ts.Item1, s] != x[ts.Item2, s]);
|
model.Add(x[team1, s] != x[team2, s]);
|
||||||
|
|
||||||
|
// Configure and run solver
|
||||||
var solver = new CpSolver();
|
var solver = new CpSolver();
|
||||||
solver.StringParameters = "max_time_in_seconds:2.0";
|
solver.StringParameters = $"max_time_in_seconds:{_maxSolveTimeSeconds}";
|
||||||
|
|
||||||
var cpSolverStatus = solver.Solve(model);
|
var cpSolverStatus = solver.Solve(model);
|
||||||
Debug.WriteLine($"Solver status: {cpSolverStatus}");
|
Debug.WriteLine($"Solver status: {cpSolverStatus}");
|
||||||
@@ -95,15 +138,13 @@ public class TeamScheduler
|
|||||||
if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
|
if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
|
||||||
return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
|
return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
|
||||||
|
|
||||||
// Debug.WriteLine($"Total cost: {solver.ObjectiveValue}\n");
|
// Extract solution: which teams are assigned to each time slot
|
||||||
|
|
||||||
foreach (var s in _timeSlots)
|
foreach (var s in _timeSlots)
|
||||||
{
|
{
|
||||||
var teams = (from t in _teams where solver.Value(x[t, s]) > 0 select _teamObjects[t]).ToArray();
|
var teams = (from t in _teams where solver.Value(x[t, s]) > 0 select _teamObjects[t]).ToArray();
|
||||||
timeSlotTeams[s] = teams;
|
timeSlotTeams[s] = teams;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Debug.WriteLine("No solution found.");
|
|
||||||
return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
|
return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,57 +2,88 @@
|
|||||||
|
|
||||||
namespace Core.Calculation;
|
namespace Core.Calculation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a solution to the team scheduling problem.
|
||||||
|
/// Contains team assignments to time slots and information about student scheduling conflicts.
|
||||||
|
/// </summary>
|
||||||
public class TeamSchedulerSolution(
|
public class TeamSchedulerSolution(
|
||||||
Team[][] timeSlots,
|
Team[][] timeSlots,
|
||||||
Student[] students,
|
Student[] students,
|
||||||
string status)
|
string status)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the solver status (e.g., "Optimal", "Feasible", "Infeasible").
|
||||||
|
/// </summary>
|
||||||
public string Status { get; } = status;
|
public string Status { get; } = status;
|
||||||
|
|
||||||
public TeamScheduleTimeSlot[] TimeSlots { get; set; }
|
/// <summary>
|
||||||
= timeSlots.Select( (teams,i) =>
|
/// Gets the scheduled time slots with team assignments and conflict information.
|
||||||
|
/// </summary>
|
||||||
|
public TeamScheduleTimeSlot[] TimeSlots { get; set; }
|
||||||
|
= timeSlots.Select( (teams,i) =>
|
||||||
new TeamScheduleTimeSlot{
|
new TeamScheduleTimeSlot{
|
||||||
Name = "Time Slot " + (i + 1),
|
Name = "Time Slot " + (i + 1),
|
||||||
Teams = teams,
|
Teams = teams,
|
||||||
StudentOverlaps = GetStudentTeamOverlaps(teams),
|
StudentOverlaps = GetStudentTeamOverlaps(teams),
|
||||||
UnscheduledStudents = GetStudentsNotInTimSlot(teams, students)
|
UnscheduledStudents = GetStudentsNotInTimSlot(teams, students)
|
||||||
}
|
}
|
||||||
).ToArray();
|
).ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the total number of student conflicts across all time slots.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeSlots">The scheduled time slots</param>
|
||||||
|
/// <returns>Total count of students with overlapping team meetings</returns>
|
||||||
public static int GetStudentTeamOverlapCount(Team[][] timeSlots)
|
public static int GetStudentTeamOverlapCount(Team[][] timeSlots)
|
||||||
{
|
{
|
||||||
return timeSlots.Sum(GetStudentTeamOverlapCount);
|
return timeSlots.Sum(GetStudentTeamOverlapCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the number of student conflicts in a single time slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeSlot">The time slot to analyze</param>
|
||||||
|
/// <returns>Count of students with multiple team meetings in this slot</returns>
|
||||||
private static int GetStudentTeamOverlapCount(Team[] timeSlot)
|
private static int GetStudentTeamOverlapCount(Team[] timeSlot)
|
||||||
{
|
{
|
||||||
return GetStudentTeamOverlaps(timeSlot).Count();
|
return GetStudentTeamOverlaps(timeSlot).Count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<Tuple<Student, IEnumerable<Team>>> GetStudentTeamOverlaps(Team[] timeSlot)
|
/// <summary>
|
||||||
|
/// Identifies students who have multiple team meetings in the same time slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeSlot">The time slot to analyze</param>
|
||||||
|
/// <returns>Students and their conflicting teams in this time slot</returns>
|
||||||
|
public static IEnumerable<(Student student, IEnumerable<Team> teams)> GetStudentTeamOverlaps(Team[] timeSlot)
|
||||||
{
|
{
|
||||||
return
|
return
|
||||||
from s in timeSlot.SelectMany(ts => ts.Students).Distinct()
|
from s in timeSlot.SelectMany(ts => ts.Students).Distinct()
|
||||||
group s by timeSlot.Where(t => t.Students.Contains(s))
|
group s by timeSlot.Where(t => t.Students.Contains(s))
|
||||||
into gs
|
into gs
|
||||||
where gs.Key.Count() > 1
|
where gs.Key.Count() > 1
|
||||||
select Tuple.Create(gs.First(), gs.Key);
|
select (gs.First(), gs.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Identifies students who have no team meetings in the given time slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeSlot">The time slot to analyze</param>
|
||||||
|
/// <param name="students">All students</param>
|
||||||
|
/// <returns>Students not scheduled in this time slot</returns>
|
||||||
public static Student[] GetStudentsNotInTimSlot(Team[] timeSlot, Student[] students)
|
public static Student[] GetStudentsNotInTimSlot(Team[] timeSlot, Student[] students)
|
||||||
{
|
{
|
||||||
var studentsInTimeSlot = timeSlot.SelectMany(ts => ts.Students).Distinct();
|
var studentsInTimeSlot = timeSlot.SelectMany(ts => ts.Students).Distinct().ToHashSet();
|
||||||
return
|
return students.Where(s => !studentsInTimeSlot.Contains(s)).ToArray();
|
||||||
(from allStudent in students
|
|
||||||
where studentsInTimeSlot.FirstOrDefault(e => e.Equals(allStudent)) == null
|
|
||||||
select allStudent
|
|
||||||
).ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the teams a student is on that weren't assigned to any time slot.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="student">The student to check</param>
|
||||||
|
/// <returns>Teams the student is on that have no scheduled meeting</returns>
|
||||||
public Team[] StudentUnassignedTeams(Student student)
|
public Team[] StudentUnassignedTeams(Student student)
|
||||||
{
|
{
|
||||||
var meetingTeams = TimeSlots.SelectMany(t => t.Teams);
|
var meetingTeams = TimeSlots.SelectMany(t => t.Teams);
|
||||||
return
|
return student.Teams.Where(e => !meetingTeams.Contains(e)).ToArray();
|
||||||
student.Teams.Where(e => !meetingTeams.Contains(e)).ToArray();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ public class TeamSchedulerTest
|
|||||||
TeamSchedulerSolution solution;
|
TeamSchedulerSolution solution;
|
||||||
if (true)
|
if (true)
|
||||||
{
|
{
|
||||||
var teamScheduler = TeamScheduler.CreateInstance(teams, 3, students);
|
var teamScheduler = new TeamScheduler(teams, 3, students);
|
||||||
solution = teamScheduler.Solve();
|
solution = teamScheduler.Solve();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -69,7 +69,7 @@ public class TeamSchedulerTest
|
|||||||
Console.WriteLine("\toverlaps");
|
Console.WriteLine("\toverlaps");
|
||||||
foreach (var overlap in overlaps)
|
foreach (var overlap in overlaps)
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"\t\t{overlap.Item1.Name} : {string.Join(", ", overlap.Item2.Select(t => t.Event.Name))}");
|
$"\t\t{overlap.student.Name} : {string.Join(", ", overlap.teams.Select(t => t.Event.Name))}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var unassigned = UnassignedStudentScheduler.UnassignedStudents(students, slot.Teams).ToList();
|
var unassigned = UnassignedStudentScheduler.UnassignedStudents(students, slot.Teams).ToList();
|
||||||
|
|||||||
@@ -12,13 +12,13 @@
|
|||||||
var allMeetingTeams = schedule.SelectMany(t => t).Distinct();
|
var allMeetingTeams = schedule.SelectMany(t => t).Distinct();
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="bluewhite p-5">
|
<div class="bluewhite p-5">
|
||||||
@{
|
@{
|
||||||
List<Tuple<Student, IEnumerable<Team>>> overlaps;
|
List<(Student student, IEnumerable<Team> teams)> overlaps;
|
||||||
}
|
}
|
||||||
@foreach (var timeslot in schedule)
|
@foreach (var timeslot in schedule)
|
||||||
{
|
{
|
||||||
overlaps = Team.GetStudentTeamOverlaps(timeslot).ToList();
|
overlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(timeslot).ToList();
|
||||||
var partialTeams = timeslot.Where(t => t is PartialTeam && t.EventDefinition.EventFormat is not EventFormat.Individual);
|
var partialTeams = timeslot.Where(t => t is PartialTeam && t.EventDefinition.EventFormat is not EventFormat.Individual);
|
||||||
var fullTeams = timeslot.Where(t => !partialTeams.Contains(t));
|
var fullTeams = timeslot.Where(t => !partialTeams.Contains(t));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@using Core.Entities
|
@using Core.Entities
|
||||||
@model Tuple<Core.Entities.Team, List<Tuple<Student, IEnumerable<Team>>>, string[]?>
|
@model Tuple<Core.Entities.Team, List<(Student student, IEnumerable<Team> teams)>, string[]?>
|
||||||
|
|
||||||
@{
|
@{
|
||||||
var team = Model.Item1;
|
var team = Model.Item1;
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
{
|
{
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
@if (overlaps.Any(t => t.Item1 == student))
|
@if (overlaps.Any(t => t.student == student))
|
||||||
{
|
{
|
||||||
<span style="color: #F66">@student.FirstName</span>
|
<span style="color: #F66">@student.FirstName</span>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -207,9 +207,25 @@
|
|||||||
{
|
{
|
||||||
_isSolving = true;
|
_isSolving = true;
|
||||||
|
|
||||||
|
// Check if there are any teams to schedule
|
||||||
|
if (!_scheduledTeams.Any())
|
||||||
|
{
|
||||||
|
_isSolving = false;
|
||||||
|
_solution = new TeamSchedulerSolution([], [], "No teams selected");
|
||||||
|
return new TableData<TeamScheduleTimeSlot> { Items = [] };
|
||||||
|
}
|
||||||
|
|
||||||
// Filter out absent students
|
// Filter out absent students
|
||||||
var availableStudents = _students.Where(s => !_absentStudents.Contains(s)).ToArray();
|
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<TeamScheduleTimeSlot> { Items = [] };
|
||||||
|
}
|
||||||
|
|
||||||
// Update parameters with absent student names
|
// Update parameters with absent student names
|
||||||
_parameters.AbsentStudents = _absentStudents.Select(s => s.FirstNameLastName).ToArray();
|
_parameters.AbsentStudents = _absentStudents.Select(s => s.FirstNameLastName).ToArray();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user