diff --git a/Core/Calculation/EventAssignment.cs b/Core/Calculation/EventAssignment.cs index 5edc7a9..fb24012 100644 --- a/Core/Calculation/EventAssignment.cs +++ b/Core/Calculation/EventAssignment.cs @@ -57,7 +57,6 @@ namespace Core.Calculation _twoTeams = events; } - public async Task Solve() { Debug.WriteLine(_parameters); @@ -78,21 +77,25 @@ namespace Core.Calculation LimitStudentAssignment(model, x); // set the range for level of effort - var eventEffortCoefficients = GetEventEffortCoefficients(_events); - SetLevelOfEffort(model, x, eventEffortCoefficients); + SetLevelOfEffort(model, x); - // each student should be assigned at least one on site activity eventDefinition + // each student should be assigned at least one on site activity if (_parameters.RequireOnSite) RequireOnSiteActivity(model, x); - - // students should have at maximum one individual eventDefinition - LimitIndividualEvent(model, x); - + // students should have at least one regional event if (_parameters.RequireRegional) RequireRegionalEvent(model, x); - OptimizeStudentEventRankings(model, x); + // students should have at maximum one individual event + AtMostOneIndividualEvent(model, x); + + //EventHasInterestedStudent(model, x); + + IndividualEventsMustBeRanked(model, x); + + OptimizeStudentEventRankings(model, x); + Debug.WriteLine("Starting optimization"); var solver = new CpSolver(); @@ -114,36 +117,34 @@ namespace Core.Calculation return eventAssignmentSolution; } + // Take the solution and map it back to the entities private List GetEventAssignments(BoolVar[,] x, CpSolver solver, CpSolverStatus cpSolverStatus) { - var eventAssignmentsList = new List(); + if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible)) + return []; - if (cpSolverStatus == CpSolverStatus.Optimal || cpSolverStatus == CpSolverStatus.Feasible) - { - foreach (var e in _allEvents) + var eventAssignments = + from e in _allEvents + let students = + from s in _allStudents + where solver.BooleanValue(x[e, s]) + select _students[s] + where students.Any() + select new Team { - var students = new List(); - foreach (var s in _allStudents) - { - if (solver.BooleanValue(x[e, s])) - { - students.Add(_students[s]); - - } - } - if (students.Count > 0) - eventAssignmentsList.Add(new Team { TeamId = _events[e].Name, Event = _events[e], Students = students}); - } - } + TeamId = _events[e].Name, + Event = _events[e], + Students = students.ToList() + }; - return eventAssignmentsList; + return eventAssignments.ToList(); } + // Maximize student event rankings private void OptimizeStudentEventRankings(CpModel model, BoolVar[,] x) { var maximizePicks = LinearExpr.NewBuilder(); - - // optimize student event rankings + foreach (var s in _allStudents) { var eventPickCoefficients = GetEventPickCoefficients(_events, _students[s].EventRankings); @@ -167,13 +168,13 @@ namespace Core.Calculation regionalEvent.Add(x[e, s]); } - //if (_parameters.RequireRegional) + // between 1 and 2 regional events model.AddLinearConstraint(LinearExpr.Sum(regionalEvent), 1, 2); regionalEvent.Clear(); } } - private void LimitIndividualEvent(CpModel model, BoolVar[,] x) + private void AtMostOneIndividualEvent(CpModel model, BoolVar[,] x) { foreach (var s in _allStudents) { @@ -206,8 +207,30 @@ namespace Core.Calculation } } - private void SetLevelOfEffort(CpModel model, BoolVar[,] x, long[] eventEffortCoefficients) + private void IndividualEventsMustBeRanked(CpModel model, BoolVar[,] x) { + foreach (var s in _allStudents) + { + var student = _students[s]; + foreach (var e in _allEvents) + { + var evt = _events[e]; + + var prohibitVar = new List { x[e, s] }; + if (evt.EventFormat == EventFormat.Individual + && student.EventRankings.Find(er => er.EventDefinition == evt) == null) + model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0); + prohibitVar.Clear(); + } + } + } + + private void SetLevelOfEffort(CpModel model, BoolVar[,] x) + { + long[] eventEffortCoefficients = _events.Select( + e => e.LevelOfEffort ?? 1L + ).ToArray(); + foreach (var s in _allStudents) { var effortVar = new BoolVar[_allEvents.Length]; @@ -234,9 +257,9 @@ namespace Core.Calculation } } + // Limit the number of events a student is assigned private void LimitStudentAssignment(CpModel model, BoolVar[,] x) { - // Limit the number of events a student is assigned foreach (var s in _allStudents) { var studentCapacity = new List(); @@ -251,6 +274,25 @@ namespace Core.Calculation } } + private void EventHasInterestedStudent(CpModel model, BoolVar[,] x) + { + foreach (var e in _allEvents) + { + var evt = _events[e]; + var studentInterest = new List(); + + foreach (var s in _allStudents) + { + var student = _students[s]; + if (student.EventRankings.Find(er => er.EventDefinition == evt) != null) + studentInterest.Add(x[e, s]); + } + model.AddLinearConstraint( + LinearExpr.Sum(studentInterest), 1, 10); + studentInterest.Clear(); + } + } + private List AddEventAssignmentThresholds(CpModel model, BoolVar[,] x) { var assignmentThresholdsList = new List(); @@ -280,6 +322,9 @@ namespace Core.Calculation ) teamCount = 1; + if (_twoTeams.Contains(evt)) + teamCount = 2; + if (evt.Name == "Tech Bowl") teamCount = 1; @@ -292,6 +337,9 @@ namespace Core.Calculation if (_droppedEvents != null && _droppedEvents.Contains(evt)) teamCount = 0; + if (evt.EventFormat == EventFormat.Individual) + evtMinTeamSize = 0; + var lb = evtMinTeamSize * teamCount; var ub = Math.Min(evtMaxTeamSize * teamCount, _parameters.TeamSizeLimit * teamCount); @@ -347,22 +395,11 @@ namespace Core.Calculation return eventRank == null ? 0L + // TODO: MaxRank can be calculated : StudentEventRanking.MaxRank - eventRank.Rank; // inverse }).ToArray(); } - private long[] GetEventEffortCoefficients(IEnumerable events) - { - return - events.Select( - e => - //e.Name == "Tech Bowl" - // ? 0 // Tech bowl gets requires no effort? - //: - e.LevelOfEffort ?? 10L - ).ToArray(); - } - public class SolutionPrinter : CpSolverSolutionCallback { private readonly IList _events; diff --git a/Core/Calculation/TeamScheduler.cs b/Core/Calculation/TeamScheduler.cs index 4184dc9..0748d35 100644 --- a/Core/Calculation/TeamScheduler.cs +++ b/Core/Calculation/TeamScheduler.cs @@ -1,41 +1,42 @@ -using Core.Entities; +using System.Diagnostics; +using Core.Entities; using Google.OrTools.Sat; namespace Core.Calculation; public class TeamScheduler { - private readonly IList _studentObjs; - private readonly IList _teamObjs; + private readonly IList _studentObjects; + private readonly IList _teamObjects; private readonly int[] _students; private readonly int[] _teams; private readonly int[] _timeSlots; - private readonly List> _scheduleSeparateTeams = new (); + private readonly List> _scheduleSeparateTeams = []; - public TeamScheduler(IList teams, int numTimeSlots) + public TeamScheduler(Team[] teams, int numTimeSlots) { - _teamObjs = teams; - _studentObjs = teams.SelectMany(t => t.Students).Distinct().ToList(); + _teamObjects = teams; + _studentObjects = teams.SelectMany(t => t.Students).Distinct().ToList(); - _students = Enumerable.Range(0, _studentObjs.Count).ToArray(); - _teams = Enumerable.Range(0, _teamObjs.Count).ToArray(); + _students = Enumerable.Range(0, _studentObjects.Count).ToArray(); + _teams = Enumerable.Range(0, _teamObjects.Count).ToArray(); _timeSlots = Enumerable.Range(0, numTimeSlots).ToArray(); } public void ScheduleSeparate(Team team1, Team team2) { - var one = _teamObjs.IndexOf(team1); - var two = _teamObjs.IndexOf(team2); + var one = _teamObjects.IndexOf(team1); + var two = _teamObjects.IndexOf(team2); _scheduleSeparateTeams.Add(Tuple.Create(one,two)); } - public static TeamScheduler CreateInstance(IList teams, int numTimeSlots) + public static TeamScheduler CreateInstance(Team[] teams, int numTimeSlots) { return new TeamScheduler(teams, numTimeSlots); } - public IList[] Solve() + public TeamSchedulerSolution Solve() { // Model. var model = new CpModel(); @@ -44,7 +45,7 @@ public class TeamScheduler var m = new int[_students.Length,_teams.Length]; foreach (var i in _students) foreach (var t in _teams) - m[i, t] = _studentObjs[i].Teams.Contains(_teamObjs[t]) ? 1 : 0; + m[i, t] = _studentObjects[i].Teams.Contains(_teamObjects[t]) ? 1 : 0; // Variables. // x - 1 if meeting of team t takes place at time slot s, else 0 @@ -82,55 +83,24 @@ public class TeamScheduler foreach (var ts in _scheduleSeparateTeams) foreach (var s in _timeSlots) model.Add(x[ts.Item1, s] != x[ts.Item2, s]); - //model.Add( - // new BoundedLinearExpression( - // x[ts.Item1, s], - // x[ts.Item2, s], - // false)); var solver = new CpSolver(); var cpSolverStatus = solver.Solve(model); - Console.WriteLine($"Solver status: {cpSolverStatus}"); + Debug.WriteLine($"Solver status: {cpSolverStatus}"); + var timeSlotTeams = new Team[_timeSlots.Length][]; - if (cpSolverStatus is CpSolverStatus.Optimal or CpSolverStatus.Feasible) - { - Console.WriteLine($"Total cost: {solver.ObjectiveValue}\n"); + if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible)) + return new TeamSchedulerSolution(timeSlotTeams, cpSolverStatus.ToString()); - //foreach (var t in _teams) - //foreach (var s in _timeSlots) - //{ - // if (solver.Value(x[t, s]) > 0) - // Console.WriteLine($"{_teamObjs[t].Name} : {s}"); - //} - - //foreach (var i in _students) - //foreach (var s in _timeSlots) - //{ - // if (solver.Value(y[i, s]) > 0) - // Console.WriteLine($"{_studentObjs[i].Name} : {s}"); - //} - - var timeSlotTeams = new List[_timeSlots.Length]; - foreach (var s in _timeSlots) - { - var teams = new List(); - foreach (var t in _teams) - { - if (solver.Value(x[t, s]) > 0) - { - teams.Add(_teamObjs[t]); - } - } - timeSlotTeams[s] = teams; - } - return timeSlotTeams; - } - else - { - //Console.WriteLine("No solution found."); - return Array.Empty>(); - } + Debug.WriteLine($"Total cost: {solver.ObjectiveValue}\n"); + foreach (var s in _timeSlots) + { + var teams = (from t in _teams where solver.Value(x[t, s]) > 0 select _teamObjects[t]).ToArray(); + timeSlotTeams[s] = teams; + } + //Debug.WriteLine("No solution found."); + return new TeamSchedulerSolution(timeSlotTeams, cpSolverStatus.ToString()); } } \ No newline at end of file diff --git a/Core/Calculation/TeamSchedulerOptions.cs b/Core/Calculation/TeamSchedulerOptions.cs new file mode 100644 index 0000000..8b9a10f --- /dev/null +++ b/Core/Calculation/TeamSchedulerOptions.cs @@ -0,0 +1,18 @@ +namespace Core.Calculation; + +public class TeamSchedulerOptions( + int timeSlots = 3, + string[]? absentStudents = null, + string[]? extended = null, + string[]? omittedEvents = null, + string[]? mustIncludeEvents = null, + DateTime date = new() +) +{ + public int TimeSlots = timeSlots; + public string[]? AbsentStudents = absentStudents; + public string[]? ExtendedTeams = extended; + public string[]? OmittedEvents = omittedEvents; + public string[]? MustIncludeEvents = mustIncludeEvents; + public DateTime Date { get; } = date == new DateTime() ? DateTime.Today : date; +} \ No newline at end of file diff --git a/Core/Calculation/TeamSchedulerSolution.cs b/Core/Calculation/TeamSchedulerSolution.cs new file mode 100644 index 0000000..7735cf2 --- /dev/null +++ b/Core/Calculation/TeamSchedulerSolution.cs @@ -0,0 +1,32 @@ +using Core.Entities; + +namespace Core.Calculation; + +public class TeamSchedulerSolution( + Team[][] timeSlots, + string status) +{ + public Team[][] TimeSlots { get; set; } = timeSlots; + public string Status { get; set; } = status; + + + public static int GetStudentTeamOverlapCount(Team[][] timeSlots) + { + return timeSlots.Sum(GetStudentTeamOverlapCount); + } + + private static int GetStudentTeamOverlapCount(Team[] timeSlot) + { + return GetStudentTeamOverlaps(timeSlot).Count(); + } + + public static IEnumerable>> GetStudentTeamOverlaps(Team[] timeSlot) + { + return + from s in timeSlot.SelectMany(ts => ts.Students).Distinct() + group s by timeSlot.Where(t => t.Students.Contains(s)) + into gs + where gs.Key.Count() > 1 + select Tuple.Create(gs.First(), gs.Key); + } +} \ No newline at end of file diff --git a/Core/Calculation/TeamScheduler_DecisionTree.cs b/Core/Calculation/TeamScheduler_DecisionTree.cs index 5e3e541..e45735d 100644 --- a/Core/Calculation/TeamScheduler_DecisionTree.cs +++ b/Core/Calculation/TeamScheduler_DecisionTree.cs @@ -3,19 +3,18 @@ namespace Core.Calculation; public class TeamScheduler_DecisionTree { - private readonly IList _students; - private readonly IList _teams; + private readonly Team[] _teams; private readonly int _timeSlotCount; - public TeamScheduler_DecisionTree(IList teams, int timeSlotCount) + public TeamScheduler_DecisionTree(Team[] teams, int timeSlotCount) { _timeSlotCount = timeSlotCount; _teams = teams; - _students = teams.SelectMany(t => t.Students).Distinct().ToList(); + //_students = teams.SelectMany(t => t.Students).Distinct().ToList(); } - public IList[] Solve() + public TeamSchedulerSolution Solve() { var timeSlots = new IList[_timeSlotCount]; for (var i = 0; i < _timeSlotCount; i++) @@ -34,68 +33,68 @@ public class TeamScheduler_DecisionTree timeSlots[overlaps.First().Item1].Add(team); } - return timeSlots; + return new TeamSchedulerSolution(timeSlots.Select(e => e.ToArray()).ToArray(), "Success?"); } - public IList[] SolveRecursive() - { - // initialize time slots - var timeSlots = new IList[_timeSlotCount]; - for (var i = 0; i < _timeSlotCount; i++) - timeSlots[i] = new List(); + //public Team[][] SolveRecursive() + //{ + // // initialize time slots + // var timeSlots = new Team[][_timeSlotCount]; + // for (var i = 0; i < _timeSlotCount; i++) + // timeSlots[i] = []; - return - (from i in Enumerable.Range(1, 5) - let solution = Recursive(timeSlots, _teams, i) - let overlapCount = Team.GetStudentTeamOverlapCount(solution) - orderby overlapCount - select solution).First(); - } + // return + // (from i in Enumerable.Range(1, 5) + // let solution = Recursive(timeSlots, _teams, i) + // let overlapCount = TeamSchedulerSolution.GetStudentTeamOverlapCount(solution.Select(e => e.ToArray()).ToArray()) + // orderby overlapCount + // select solution).First(); + //} - private static IList[] CopyTimeSlots(IList[] timeSlots) - { - return timeSlots.Select(ts => (IList)new List(ts)).ToArray(); - } + //private static Team[][] CopyTimeSlots(Team[][] timeSlots) + //{ + // return timeSlots.Select(Team[] (ts) => ts.ToArray()).ToArray(); + //} - private static IList[] Recursive(IList[] timeSlots, IList teams, int overlapTriesStudents = 4) - { - if (!teams.Any()) - return timeSlots; - - var overlapsTries = - (from team in teams.OrderByDescending(t => t.Students.Count).Take(overlapTriesStudents) - //from ts in timeSlots - from tsi in Enumerable.Range(0, timeSlots.Length) - let ts = timeSlots[tsi] - let tss = ts.SelectMany(t => t.Students) - select Tuple.Create(ts, tsi, team, team.Students.Count(tss.Contains))) - .OrderBy(t => t.Item4) - .ThenBy(t => t.Item1.Count); + //private static Team[][] Recursive(Team[][] timeSlots, Team[] teams, int overlapTriesStudents = 4) + //{ + // if (!teams.Any()) + // return timeSlots; - var minOverlaps = - (from o in overlapsTries - group o by o.Item4 - into oo - orderby oo.Key - select oo).First(); + // var overlapsTries = + // (from team in teams.OrderByDescending(t => t.Students.Count).Take(overlapTriesStudents) + // //from ts in timeSlots + // from tsi in Enumerable.Range(0, timeSlots.Length) + // let ts = timeSlots[tsi] + // let tss = ts.SelectMany(t => t.Students) + // select Tuple.Create(ts, tsi, team, team.Students.Count(tss.Contains))) + // .OrderBy(t => t.Item4) + // .ThenBy(t => t.Item1.Length); - var first = minOverlaps.First(); + // var minOverlaps = + // (from o in overlapsTries + // group o by o.Item4 + // into oo + // orderby oo.Key + // select oo).First(); - var results = new List[], int>>(); + // var first = minOverlaps.First(); - foreach (var minOverlap in minOverlaps) - { - var timeSlotsAfterAdd = CopyTimeSlots(timeSlots); - timeSlotsAfterAdd[first.Item2].Add(first.Item3); - var remainingTeams = teams.Where(t => t != first.Item3).ToList(); + // var results = new List[]>, int>>(); - var result = Recursive(timeSlotsAfterAdd, remainingTeams); + // foreach (var minOverlap in minOverlaps) + // { + // var timeSlotsAfterAdd = CopyTimeSlots(timeSlots); + // timeSlotsAfterAdd[first.Item2].Add(first.Item3); + // var remainingTeams = teams.Where(t => t != first.Item3).ToArray(); - results.Add(Tuple.Create(result, Team.GetStudentTeamOverlapCount(result))); - if (minOverlap.Item4 == 0) - break; - } + // var result = Recursive(timeSlotsAfterAdd.Select(e => e.ToArray()).ToArray(), remainingTeams); - return results.OrderByDescending(r => r.Item2).First().Item1; - } + // results.Add(Tuple.Create(result, TeamSchedulerSolution.GetStudentTeamOverlapCount(result))); + // if (minOverlap.Item4 == 0) + // break; + // } + + // return results.OrderByDescending(r => r.Item2).First().Item1; + //} } \ No newline at end of file diff --git a/Core/Calculation/UnassignedStudentScheduler.cs b/Core/Calculation/UnassignedStudentScheduler.cs index 4139cf7..48e9cd3 100644 --- a/Core/Calculation/UnassignedStudentScheduler.cs +++ b/Core/Calculation/UnassignedStudentScheduler.cs @@ -4,49 +4,42 @@ namespace Core.Calculation; public class UnassignedStudentScheduler { - private readonly IList _students; - private readonly IList _teams; - private readonly IList[] _timeslots; + private readonly Student[] _students; + private readonly Team[] _teams; + private readonly IList[] _timeSlots; - public UnassignedStudentScheduler(IList teams, IList[] timeslots) + public UnassignedStudentScheduler(Team[] teams, Team[][] timeslots) { _teams = teams; - _students = teams.SelectMany(t => t.Students).Distinct().ToList(); - _timeslots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToList()).ToArray(); + _students = teams.SelectMany(t => t.Students).Distinct().ToArray(); + _timeSlots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToArray()).ToArray(); } public static IEnumerable UnassignedStudents(IList students, IList timeSlot) => students.Where(s => !timeSlot.SelectMany(t => t.Students).Contains(s)); - public static IEnumerable[] UnassignedStudents(IList students, IList[] schedule) => schedule.Select(ts => UnassignedStudents(students, ts)).ToArray(); + public TeamSchedulerSolution ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy) + { + var ss = scheduleStrategy switch + { + UnassignedScheduleStrategy.BiggestGroup => ScheduleStrategy(GetAvailableTeams_BiggestGroup), + UnassignedScheduleStrategy.IndividualEvents => ScheduleStrategy(GetAvailableTeams_Individual), + UnassignedScheduleStrategy.AnyNotMeetingAlready => ScheduleStrategy(GetAvailableTeams_AnyNotMeetingAlready), + UnassignedScheduleStrategy.Any => ScheduleStrategy(GetAvailableTeams_Any), + UnassignedScheduleStrategy.LevelOfEffort => ScheduleStrategy(GetAvailableTeams_LevelOfEffort), + _ => throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null) + }; - public IList[] ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy) - { - switch (scheduleStrategy) - { - case UnassignedScheduleStrategy.BiggestGroup: - return ScheduleStrategy(GetAvailableTeams_BiggestGroup); - case UnassignedScheduleStrategy.IndividualEvents: - return ScheduleStrategy(GetAvailableTeams_Individual); - case UnassignedScheduleStrategy.AnyNotMeetingAlready: - return ScheduleStrategy(GetAvailableTeams_AnyNotMeetingAlready); - case UnassignedScheduleStrategy.Any: - return ScheduleStrategy(GetAvailableTeams_Any); - case UnassignedScheduleStrategy.LevelOfEffort: - return ScheduleStrategy(GetAvailableTeams_LevelOfEffort); - default: - throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null); - } - } + return new TeamSchedulerSolution(ss, "Success?"); + } - - public IList[] ScheduleStrategy(Func, IEnumerable, IEnumerable> availableTeamSelector) + public Team[][] ScheduleStrategy(Func, IEnumerable, IEnumerable> availableTeamSelector) { // Find stuff for unassigned students in each timeslot - var scheduledTeams = _timeslots.SelectMany(list => list).Distinct().ToList(); - foreach (var slot in _timeslots) + var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList(); + foreach (var slot in _timeSlots) { var unassigned = UnassignedStudents(_students, slot).ToList(); while (unassigned.Count > 0) @@ -67,15 +60,14 @@ public class UnassignedStudentScheduler } } - return _timeslots; + return _timeSlots.Select(e => e.ToArray()).ToArray(); } - // find teams where several unassigned students can work together private IEnumerable GetAvailableTeams_BiggestGroup( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams - .Where(t => scheduledTeams.All(st => st.Name != t.Name)) + .Where(t => scheduledTeams.All(st => st.TeamId != t.TeamId)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual //.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name)) @@ -86,7 +78,7 @@ public class UnassignedStudentScheduler private IEnumerable GetAvailableTeams_Individual( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams - .Where(t => scheduledTeams.All(st => st.Name != t.Name)) + .Where(t => scheduledTeams.All(st => st.TeamId != t.TeamId)) .Where(t => t.Event.EventFormat == EventFormat.Individual || t.Students.Count == 1) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); @@ -95,7 +87,7 @@ public class UnassignedStudentScheduler private IEnumerable GetAvailableTeams_AnyNotMeetingAlready( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams - .Where(t => scheduledTeams.All(st => st.Name != t.Name)) + .Where(t => scheduledTeams.All(st => st.TeamId != t.TeamId)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 0); @@ -110,7 +102,7 @@ public class UnassignedStudentScheduler private IEnumerable GetAvailableTeams_LevelOfEffort( IEnumerable scheduledTeams, IEnumerable assignedStudents) => _teams - .Where(t => scheduledTeams.All(st => st.Name != t.Name)) + .Where(t => scheduledTeams.All(st => st.TeamId != t.TeamId)) .Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual //.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name)) @@ -120,38 +112,38 @@ public class UnassignedStudentScheduler public IList[] AddAdditionalTimeSlot(Team team) { // sort by how many teammembers are already in that timeslot, descending - foreach (var timeslot in _timeslots.OrderBy(ts => ts.SelectMany(t => t.Students).Count(team.Students.Contains))) + foreach (var timeslot in _timeSlots.OrderBy(ts => ts.SelectMany(t => t.Students).Count(team.Students.Contains))) { - if (timeslot.Any(t => t.Name == team.Name)) + if (timeslot.Any(t => t.TeamId == team.TeamId)) continue; timeslot.Add(team); break; } - return _timeslots; + return _timeSlots; } // Keep the current events rolling for the other time slots public IList[] ExtendEvents() { - var scheduledTeams = _timeslots.SelectMany(list => list).Distinct().ToList(); + var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList(); // get all the students in each time slot - var timeslotStudents = _timeslots.Select(ts => ts.SelectMany(t => t.Students).Distinct()).ToArray(); + var timeslotStudents = _timeSlots.Select(ts => ts.SelectMany(t => t.Students).Distinct()).ToArray(); // clone teams from timeslot for each other timeslot, removing the students //var copiedTeams = new Dictionary>(); - for (var i = 0; i < _timeslots.Length; i++) + for (var i = 0; i < _timeSlots.Length; i++) { - var sourceTimeslot = _timeslots[i]; + var sourceTimeslot = _timeSlots[i]; - for (var j = 0; j < _timeslots.Length; j++) + for (var j = 0; j < _timeSlots.Length; j++) { if (i == j) continue; - var targetTimeslot = _timeslots[j]; + var targetTimeslot = _timeSlots[j]; var targetTimeslotStudents = targetTimeslot.SelectMany(t => t.Students).Distinct(); var clonedTeams = sourceTimeslot.Select(t => t.CloneWithOmittedStudents(targetTimeslotStudents)); foreach (var clonedTeam in clonedTeams.Where(t => t.Students.Any())) @@ -161,6 +153,6 @@ public class UnassignedStudentScheduler } } - return _timeslots; + return _timeSlots; } } \ No newline at end of file diff --git a/Core/Entities/EventDefinition.cs b/Core/Entities/EventDefinition.cs index 97f9e29..55554c5 100644 --- a/Core/Entities/EventDefinition.cs +++ b/Core/Entities/EventDefinition.cs @@ -32,7 +32,10 @@ public class EventDefinition || SemifinalistActivity.Contains("Speech") || SemifinalistActivity.Contains("Test") || SemifinalistActivity.Contains("Flight") + || Name.Contains("Leadership") + || Name.Contains("Forensic") || Name.Contains("Flight") + || Name.Contains("Coding") || SemifinalistActivity.Contains("Debate") || SemifinalistActivity.Contains("Photography") || SemifinalistActivity.Contains("Build") diff --git a/Core/Entities/PartialTeam.cs b/Core/Entities/PartialTeam.cs index 99385e7..46f2d85 100644 --- a/Core/Entities/PartialTeam.cs +++ b/Core/Entities/PartialTeam.cs @@ -8,6 +8,6 @@ public class PartialTeam : Team { var remainingStudents = Students.Where(s => !studentsToOmit.Contains(s)).ToList(); var omittedStudents = OmittedStudents.Union(Students.Where(studentsToOmit.Contains)).Distinct().ToList(); - return new PartialTeam{TeamId = Name, Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents }; + return new PartialTeam{TeamId = TeamId, Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents }; } } \ No newline at end of file diff --git a/Core/Entities/Student.cs b/Core/Entities/Student.cs index c8fdf8d..f17a39c 100644 --- a/Core/Entities/Student.cs +++ b/Core/Entities/Student.cs @@ -3,7 +3,7 @@ using static System.Text.RegularExpressions.Regex; namespace Core.Entities; -public class Student +public class Student : IEquatable { public int Id { get; set; } @@ -74,4 +74,23 @@ public class Student public bool VotingDelegate => OfficerRole is Entities.OfficerRole.President or Entities.OfficerRole.VicePresident; + public bool Equals(Student? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id; + } + + public override bool Equals(object? obj) + { + if (obj is null) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((Student)obj); + } + + public override int GetHashCode() + { + return Id; + } } \ No newline at end of file diff --git a/Core/Entities/StudentEventRanking.cs b/Core/Entities/StudentEventRanking.cs index ff483e4..4ab3544 100644 --- a/Core/Entities/StudentEventRanking.cs +++ b/Core/Entities/StudentEventRanking.cs @@ -7,6 +7,6 @@ public class StudentEventRanking public EventDefinition EventDefinition { get; set; } = null!; public int Rank { get; set; } - public const int MaxRank = 6; + public const int MaxRank = 10; } \ No newline at end of file diff --git a/Core/Entities/Team.cs b/Core/Entities/Team.cs index 204374c..d0b76f3 100644 --- a/Core/Entities/Team.cs +++ b/Core/Entities/Team.cs @@ -5,10 +5,15 @@ public class Team { public int Id { get; set; } - [Required] + [Required] [Display(Name = "Team Name")] - public string Name { get; set; } - public EventDefinition Event { get; set; } + public int? Number { get; set; } + public EventDefinition Event + { + get; + set; + } + public List Students { get; set; } = []; public Student? Captain { get; set; } @@ -16,8 +21,6 @@ public class Team [Display(Name = "Team Id")] public string? TeamId { get; set; } - //public string? RegionalTimeSlot { get; set; } - // public Tuple? RegionalTimeSlotObj //{ @@ -51,46 +54,17 @@ public class Team var studentsToOmitList = studentsToOmit.ToList(); var omittedStudents = Students.Where(studentsToOmitList.Contains).ToList(); if (!omittedStudents.Any()) - return new Team{Captain = Captain, Event = Event, Students = Students.ToList(), TeamId = Name}; + return new Team{Captain = Captain, Event = Event, Students = Students.ToList(), TeamId = TeamId, Number = Number}; var remainingStudents = Students.Where(s => !studentsToOmitList.Contains(s)).ToList(); - return new PartialTeam { Name = Name, Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents}; + return new PartialTeam { Number = Number, Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents}; } public Team Clone() => CloneWithOmittedStudents([]); - public static int GetStudentTeamOverlapCount(IList[] timeSlots) - { - return timeSlots.Sum(GetStudentTeamOverlapCount); - } - - private static int GetStudentTeamOverlapCount(IList timeSlot) - { - return GetStudentTeamOverlaps(timeSlot).Count(); - } - - public static IEnumerable>> GetStudentTeamOverlaps(IList timeSlot) - { - return - from s in timeSlot.SelectMany(ts => ts.Students).Distinct() - group s by timeSlot.Where(t => t.Students.Contains(s)) - into gs - where gs.Key.Count() > 1 - select Tuple.Create(gs.First(), gs.Key); - } public override string ToString() { - return Name; - } - - public string ToStringWithIndividualAndRegional() - { - var ind = Event.EventFormat is EventFormat.Individual ? " (Ind.)" : string.Empty; - var regional= Event.RegionalEvent ? " (Reg.)" : string.Empty; - //var regional= Event.RegionalEvent ? " (Reg.)" : string.Empty; - - var eventAttributes = Event.EventAttributes(); - return string.IsNullOrEmpty(eventAttributes) ? Name : Name + " (" + eventAttributes + ")"; + return $"{Event.Name} {(Number != null ? $"(#{Number})" : "")}"; } } \ No newline at end of file diff --git a/Core/Parsers/EventDefinitionParser.cs b/Core/Parsers/EventDefinitionParser.cs index 5bd70bb..a26e576 100644 --- a/Core/Parsers/EventDefinitionParser.cs +++ b/Core/Parsers/EventDefinitionParser.cs @@ -49,7 +49,7 @@ public class EventDefinitionParser : CsvParserBase var theme = CsvReader.GetField("Theme"); var description = CsvReader.GetField("Description"); var levelOfEffort = CsvReader.GetField("Level of Effort"); - //var regionalTeams = CsvReader.GetField("Regional Teams"); + //var regionalTeams = CsvReader.GetField("Regional TimeSlots"); var competitiveEvent = new EventDefinition { diff --git a/Core/Parsers/TeamParser.cs b/Core/Parsers/TeamParser.cs index 659d209..d431ce7 100644 --- a/Core/Parsers/TeamParser.cs +++ b/Core/Parsers/TeamParser.cs @@ -39,7 +39,7 @@ namespace Core.Parsers foreach (var team in _teams) { - csv.WriteField(team.Name); + csv.WriteField(team.TeamId); csv.WriteField(team.Event.Name); foreach (var teamStudent in team.Students) { @@ -146,14 +146,14 @@ namespace Core.Parsers { if (@event.EventFormat is EventFormat.Team) { - teams.Add(new Team { Event = @event, Students = teamStudents, Captain = captain, Name = teamName, TeamId = teamNumber}); + teams.Add(new Team { Event = @event, Students = teamStudents, Captain = captain, TeamId = teamNumber}); } else if (@event.EventFormat is EventFormat.Individual) { foreach (var student in teamStudents) { - teams.Add(new Team{Name = $"{teamName} - {student.FirstName}", Event = @event, - Students = new List { student }, Captain = student, TeamId = teamNumber}); + teams.Add(new Team{Event = @event, + Students = [student], Captain = student, TeamId = teamNumber}); } } } diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index 7b7b55e..2678613 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -73,6 +73,9 @@ namespace Data .WithMany(e => e.Teams); builder.HasOne(e => e.Captain); + + builder.Property(e => e.Number).IsRequired(false); + //builder.i } } public class StudentEventRankingConfiguration : IEntityTypeConfiguration diff --git a/Data/Migrations/20250825173042_InitialCreate.cs b/Data/Migrations/20250825173042_InitialCreate.cs index 6f20f30..862c99f 100644 --- a/Data/Migrations/20250825173042_InitialCreate.cs +++ b/Data/Migrations/20250825173042_InitialCreate.cs @@ -54,7 +54,7 @@ namespace Data.Migrations }); migrationBuilder.CreateTable( - name: "Teams", + name: "TimeSlots", columns: table => new { Id = table.Column(type: "INTEGER", nullable: false) @@ -91,7 +91,7 @@ namespace Data.Migrations table.ForeignKey( name: "FK_StudentTeam_Teams_TeamsId", column: x => x.TeamsId, - principalTable: "Teams", + principalTable: "TimeSlots", principalColumn: "Id", onDelete: ReferentialAction.Cascade); }); @@ -108,7 +108,7 @@ namespace Data.Migrations migrationBuilder.CreateIndex( name: "IX_Teams_CaptainId", - table: "Teams", + table: "TimeSlots", column: "CaptainId"); } @@ -122,7 +122,7 @@ namespace Data.Migrations name: "StudentTeam"); migrationBuilder.DropTable( - name: "Teams"); + name: "TimeSlots"); migrationBuilder.DropTable( name: "Students"); diff --git a/Data/Migrations/20250827183503_StudentCleanup.cs b/Data/Migrations/20250827183503_StudentCleanup.cs index f30f9e9..a112a89 100644 --- a/Data/Migrations/20250827183503_StudentCleanup.cs +++ b/Data/Migrations/20250827183503_StudentCleanup.cs @@ -12,7 +12,7 @@ namespace Data.Migrations { migrationBuilder.RenameColumn( name: "TeamNumber", - table: "Teams", + table: "TimeSlots", newName: "TeamId"); migrationBuilder.RenameColumn( @@ -36,7 +36,7 @@ namespace Data.Migrations { migrationBuilder.RenameColumn( name: "TeamId", - table: "Teams", + table: "TimeSlots", newName: "TeamNumber"); migrationBuilder.RenameColumn( diff --git a/Data/Migrations/20250903121202_StudentAddEmailPhone.cs b/Data/Migrations/20250903121202_StudentAddEmailPhone.cs index b354b2d..400b8fd 100644 --- a/Data/Migrations/20250903121202_StudentAddEmailPhone.cs +++ b/Data/Migrations/20250903121202_StudentAddEmailPhone.cs @@ -12,14 +12,14 @@ namespace Data.Migrations { migrationBuilder.AddColumn( name: "EventId", - table: "Teams", + table: "TimeSlots", type: "INTEGER", nullable: false, defaultValue: 0); migrationBuilder.AddColumn( name: "Name", - table: "Teams", + table: "TimeSlots", type: "TEXT", nullable: false, defaultValue: ""); @@ -47,12 +47,12 @@ namespace Data.Migrations migrationBuilder.CreateIndex( name: "IX_Teams_EventId", - table: "Teams", + table: "TimeSlots", column: "EventId"); migrationBuilder.AddForeignKey( name: "FK_Teams_Events_EventId", - table: "Teams", + table: "TimeSlots", column: "EventId", principalTable: "Events", principalColumn: "Id", @@ -64,19 +64,19 @@ namespace Data.Migrations { migrationBuilder.DropForeignKey( name: "FK_Teams_Events_EventId", - table: "Teams"); + table: "TimeSlots"); migrationBuilder.DropIndex( name: "IX_Teams_EventId", - table: "Teams"); + table: "TimeSlots"); migrationBuilder.DropColumn( name: "EventId", - table: "Teams"); + table: "TimeSlots"); migrationBuilder.DropColumn( name: "Name", - table: "Teams"); + table: "TimeSlots"); migrationBuilder.DropColumn( name: "Email", diff --git a/Data/Migrations/20250915175030_AddTeamNumber.Designer.cs b/Data/Migrations/20250915175030_AddTeamNumber.Designer.cs new file mode 100644 index 0000000..edfe032 --- /dev/null +++ b/Data/Migrations/20250915175030_AddTeamNumber.Designer.cs @@ -0,0 +1,255 @@ +// +using Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250915175030_AddTeamNumber")] + partial class AddTeamNumber + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("Core.Entities.EventDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Documentation") + .HasColumnType("TEXT"); + + b.Property("Eligibility") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventFormat") + .HasColumnType("INTEGER"); + + b.Property("LevelOfEffort") + .HasColumnType("INTEGER"); + + b.Property("MaxTeamCountState") + .HasColumnType("INTEGER"); + + b.Property("MaxTeamSize") + .HasColumnType("INTEGER"); + + b.Property("MinTeamSize") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("RegionalEvent") + .HasColumnType("INTEGER"); + + b.Property("RegionalPresubmit") + .HasColumnType("INTEGER"); + + b.Property("SemifinalistActivity") + .HasColumnType("TEXT"); + + b.Property("ShortName") + .HasColumnType("TEXT"); + + b.Property("StatePreliminaryRound") + .HasColumnType("INTEGER"); + + b.Property("StatePresubmission") + .HasColumnType("INTEGER"); + + b.Property("StatePretesting") + .HasColumnType("INTEGER"); + + b.Property("Theme") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Core.Entities.Student", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Grade") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("NationalId") + .HasColumnType("TEXT"); + + b.Property("OfficerRole") + .HasColumnType("INTEGER"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("RegionalId") + .HasColumnType("TEXT"); + + b.Property("StateId") + .HasColumnType("TEXT"); + + b.Property("TsaYear") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("Core.Entities.StudentEventRanking", b => + { + b.Property("EventDefinitionId") + .HasColumnType("INTEGER"); + + b.Property("StudentId") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.HasKey("EventDefinitionId", "StudentId"); + + b.HasIndex("StudentId"); + + b.ToTable("StudentEventRanking"); + }); + + modelBuilder.Entity("Core.Entities.Team", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CaptainId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CaptainId"); + + b.HasIndex("EventId"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("StudentTeam", b => + { + b.Property("StudentsId") + .HasColumnType("INTEGER"); + + b.Property("TeamsId") + .HasColumnType("INTEGER"); + + b.HasKey("StudentsId", "TeamsId"); + + b.HasIndex("TeamsId"); + + b.ToTable("StudentTeam"); + }); + + modelBuilder.Entity("Core.Entities.StudentEventRanking", b => + { + b.HasOne("Core.Entities.EventDefinition", "EventDefinition") + .WithMany() + .HasForeignKey("EventDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Student", "Student") + .WithMany("EventRankings") + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventDefinition"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Core.Entities.Team", b => + { + b.HasOne("Core.Entities.Student", "Captain") + .WithMany() + .HasForeignKey("CaptainId"); + + b.HasOne("Core.Entities.EventDefinition", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Captain"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("StudentTeam", b => + { + b.HasOne("Core.Entities.Student", null) + .WithMany() + .HasForeignKey("StudentsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Team", null) + .WithMany() + .HasForeignKey("TeamsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.Student", b => + { + b.Navigation("EventRankings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/20250915175030_AddTeamNumber.cs b/Data/Migrations/20250915175030_AddTeamNumber.cs new file mode 100644 index 0000000..8aa8ebc --- /dev/null +++ b/Data/Migrations/20250915175030_AddTeamNumber.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations +{ + /// + public partial class AddTeamNumber : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Name", + table: "TimeSlots"); + + migrationBuilder.AddColumn( + name: "Number", + table: "TimeSlots", + type: "INTEGER", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Number", + table: "TimeSlots"); + + migrationBuilder.AddColumn( + name: "Name", + table: "TimeSlots", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/Data/Migrations/20250917165027_NullableTeamNumber.Designer.cs b/Data/Migrations/20250917165027_NullableTeamNumber.Designer.cs new file mode 100644 index 0000000..21721eb --- /dev/null +++ b/Data/Migrations/20250917165027_NullableTeamNumber.Designer.cs @@ -0,0 +1,255 @@ +// +using Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Data.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20250917165027_NullableTeamNumber")] + partial class NullableTeamNumber + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("Core.Entities.EventDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Documentation") + .HasColumnType("TEXT"); + + b.Property("Eligibility") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EventFormat") + .HasColumnType("INTEGER"); + + b.Property("LevelOfEffort") + .HasColumnType("INTEGER"); + + b.Property("MaxTeamCountState") + .HasColumnType("INTEGER"); + + b.Property("MaxTeamSize") + .HasColumnType("INTEGER"); + + b.Property("MinTeamSize") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("RegionalEvent") + .HasColumnType("INTEGER"); + + b.Property("RegionalPresubmit") + .HasColumnType("INTEGER"); + + b.Property("SemifinalistActivity") + .HasColumnType("TEXT"); + + b.Property("ShortName") + .HasColumnType("TEXT"); + + b.Property("StatePreliminaryRound") + .HasColumnType("INTEGER"); + + b.Property("StatePresubmission") + .HasColumnType("INTEGER"); + + b.Property("StatePretesting") + .HasColumnType("INTEGER"); + + b.Property("Theme") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Name"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("Core.Entities.Student", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Grade") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("NationalId") + .HasColumnType("TEXT"); + + b.Property("OfficerRole") + .HasColumnType("INTEGER"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("RegionalId") + .HasColumnType("TEXT"); + + b.Property("StateId") + .HasColumnType("TEXT"); + + b.Property("TsaYear") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Students"); + }); + + modelBuilder.Entity("Core.Entities.StudentEventRanking", b => + { + b.Property("EventDefinitionId") + .HasColumnType("INTEGER"); + + b.Property("StudentId") + .HasColumnType("INTEGER"); + + b.Property("Rank") + .HasColumnType("INTEGER"); + + b.HasKey("EventDefinitionId", "StudentId"); + + b.HasIndex("StudentId"); + + b.ToTable("StudentEventRanking"); + }); + + modelBuilder.Entity("Core.Entities.Team", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CaptainId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.Property("TeamId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CaptainId"); + + b.HasIndex("EventId"); + + b.ToTable("Teams"); + }); + + modelBuilder.Entity("StudentTeam", b => + { + b.Property("StudentsId") + .HasColumnType("INTEGER"); + + b.Property("TeamsId") + .HasColumnType("INTEGER"); + + b.HasKey("StudentsId", "TeamsId"); + + b.HasIndex("TeamsId"); + + b.ToTable("StudentTeam"); + }); + + modelBuilder.Entity("Core.Entities.StudentEventRanking", b => + { + b.HasOne("Core.Entities.EventDefinition", "EventDefinition") + .WithMany() + .HasForeignKey("EventDefinitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Student", "Student") + .WithMany("EventRankings") + .HasForeignKey("StudentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EventDefinition"); + + b.Navigation("Student"); + }); + + modelBuilder.Entity("Core.Entities.Team", b => + { + b.HasOne("Core.Entities.Student", "Captain") + .WithMany() + .HasForeignKey("CaptainId"); + + b.HasOne("Core.Entities.EventDefinition", "Event") + .WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Captain"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("StudentTeam", b => + { + b.HasOne("Core.Entities.Student", null) + .WithMany() + .HasForeignKey("StudentsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Core.Entities.Team", null) + .WithMany() + .HasForeignKey("TeamsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Core.Entities.Student", b => + { + b.Navigation("EventRankings"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Data/Migrations/20250917165027_NullableTeamNumber.cs b/Data/Migrations/20250917165027_NullableTeamNumber.cs new file mode 100644 index 0000000..ba40250 --- /dev/null +++ b/Data/Migrations/20250917165027_NullableTeamNumber.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations +{ + /// + public partial class NullableTeamNumber : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Number", + table: "TimeSlots", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "Number", + table: "TimeSlots", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + } + } +} diff --git a/Data/Migrations/AppDbContextModelSnapshot.cs b/Data/Migrations/AppDbContextModelSnapshot.cs index b9eb182..f303890 100644 --- a/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/Data/Migrations/AppDbContextModelSnapshot.cs @@ -161,9 +161,8 @@ namespace Data.Migrations b.Property("EventId") .HasColumnType("INTEGER"); - b.Property("Name") - .IsRequired() - .HasColumnType("TEXT"); + b.Property("Number") + .HasColumnType("INTEGER"); b.Property("TeamId") .HasColumnType("TEXT"); @@ -174,7 +173,7 @@ namespace Data.Migrations b.HasIndex("EventId"); - b.ToTable("Teams"); + b.ToTable("TimeSlots"); }); modelBuilder.Entity("StudentTeam", b => diff --git a/DataBackup/ChapterOrganizer.db b/DataBackup/ChapterOrganizer.db index 1589c59..ab25e68 100644 Binary files a/DataBackup/ChapterOrganizer.db and b/DataBackup/ChapterOrganizer.db differ diff --git a/Tests/Calculation/TeamSchedulerTest.cs b/Tests/Calculation/TeamSchedulerTest.cs index eb6863b..f38db74 100644 --- a/Tests/Calculation/TeamSchedulerTest.cs +++ b/Tests/Calculation/TeamSchedulerTest.cs @@ -36,40 +36,40 @@ public class TeamSchedulerTest //var eventAssignment = new EventAssignment(events, students); //var teams = eventAssignment.Solve(); - IList[] timeSlots; + TeamSchedulerSolution solution; if (true) { var teamScheduler = TeamScheduler.CreateInstance(teams, 3); - timeSlots = teamScheduler.Solve(); + solution = teamScheduler.Solve(); } else { var teamScheduler = new TeamScheduler_DecisionTree(teams, 3); - timeSlots = teamScheduler.Solve(); + solution = teamScheduler.Solve(); } - timeSlots = new UnassignedStudentScheduler(allTeams, timeSlots).ScheduleStrategy(UnassignedScheduleStrategy.BiggestGroup); - timeSlots = new UnassignedStudentScheduler(allTeams, timeSlots).ScheduleStrategy(UnassignedScheduleStrategy.IndividualEvents); + solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.BiggestGroup); + solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.IndividualEvents); var i = 1; - foreach (var slot in timeSlots) + foreach (var slot in solution.TimeSlots) { Console.WriteLine($"Time slot {i++}"); foreach (var team in slot.OrderBy(s => s.Event.Name)) { var names = string.Join(", ", team.Students.OrderByDescending(s => s.Grade + s.TsaYear).Select(s => s.FirstName)); - Console.WriteLine($"\t{team.Name}"); + Console.WriteLine($"\t{team.Event.Name}"); Console.WriteLine($"\t\t{names}"); } - var overlaps = Team.GetStudentTeamOverlaps(slot).ToList(); + var overlaps = TeamSchedulerSolution.GetStudentTeamOverlaps(slot).ToList(); if (overlaps.Any()) { Console.WriteLine("\toverlaps"); foreach (var overlap in overlaps) Console.WriteLine( - $"\t\t{overlap.Item1.Name} : {string.Join(", ", overlap.Item2.Select(t => t.Name))}"); + $"\t\t{overlap.Item1.Name} : {string.Join(", ", overlap.Item2.Select(t => t.Event.Name))}"); } var unassigned = UnassignedStudentScheduler.UnassignedStudents(students, slot).ToList(); diff --git a/Tests/Parsers/TeamParser_Tests.cs b/Tests/Parsers/TeamParser_Tests.cs index e88d3ab..a71ec30 100644 --- a/Tests/Parsers/TeamParser_Tests.cs +++ b/Tests/Parsers/TeamParser_Tests.cs @@ -11,7 +11,7 @@ public class TeamParser_Tests foreach (var team in teams) { - Console.WriteLine($"{team.Name}"); + Console.WriteLine($"{team.Event.Name}"); var join = string.Join(", ", team.Students.OrderByDescending(s=> s.Grade + s.TsaYear).Select(s => $"{s.FirstNameLastName}{(team.Captain == s ? " *" : "")}")); Console.WriteLine($"\t{join}"); @@ -29,7 +29,7 @@ public class TeamParser_Tests foreach (var team in teams.Where(t => t.Event.RegionalEvent)) { - Console.WriteLine($"{team.Name} {team.Event.RegionalPresubmit} team.RegionalTimeSlot"); + Console.WriteLine($"{team.Event.Name} {team.Event.RegionalPresubmit} team.RegionalTimeSlot"); var join = string.Join(", ", team.Students.OrderByDescending(s => team.Captain == s).ThenByDescending(s => s.Grade + s.TsaYear).Select(s => $"{s.FirstNameLastName}{(team.Captain == s ? " *" + "(Cpt.)" : "")}")); Console.WriteLine($"\t{join}"); diff --git a/Tests/Parsers/TestEntityHandler.cs b/Tests/Parsers/TestEntityHandler.cs index 183fdcb..198d693 100644 --- a/Tests/Parsers/TestEntityHandler.cs +++ b/Tests/Parsers/TestEntityHandler.cs @@ -11,7 +11,7 @@ public static class TestEntityHandler public static EventDefinition[] GetEvents() { - var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2025-26 RMS TSA - Event Definitions.csv"); + var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2023-24 RMS TSA student & event - Event Definitions.csv"); var eventRankingsParser = new EventDefinitionParser(fileInfo); return eventRankingsParser.Parse(); } @@ -24,7 +24,7 @@ public static class TestEntityHandler public static Student[] GetStudents(IList events) { //var studentEventRankingsCsv = "Student Event Rankings.csv"; - var studentEventRankingsCsv = "2025-26 RMS TSA student & eventDefinition - Nationals Student Event Rankings.csv"; + var studentEventRankingsCsv = "2023-24 RMS TSA student & event - Student Event Rankings.csv"; var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv); var eventRankingsParser = new StudentParser(fileInfo); @@ -35,7 +35,7 @@ public static class TestEntityHandler public static Team[] GetTeams(IList events, IList students) { //var studentEventRankingsCsv = "Student Event Rankings.csv"; - var studentEventRankingsCsv = "2025-26 RMS TSA Teams.csv"; + var studentEventRankingsCsv = "2023-24 RMS TSA student & event - TimeSlots.csv"; var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv); var eventRankingsParser = new TeamParser(fileInfo); diff --git a/WebApp/Components/App.razor b/WebApp/Components/App.razor index e59af95..d03f3ab 100644 --- a/WebApp/Components/App.razor +++ b/WebApp/Components/App.razor @@ -8,7 +8,6 @@ - diff --git a/WebApp/Components/Layout/CustomThemes.cs b/WebApp/Components/Layout/CustomThemes.cs new file mode 100644 index 0000000..bcfef38 --- /dev/null +++ b/WebApp/Components/Layout/CustomThemes.cs @@ -0,0 +1,278 @@ +using MudBlazor; + +namespace WebApp.Components.Layout +{ + public static class CustomThemes + { + /* + ! + * Bootswatch v5.3.3 (https://bootswatch.com) + * Theme: cerulean + * Copyright 2012-2024 Thomas Park + * Licensed under MIT + * Based on Bootstrap + */ + public static readonly MudTheme Ceruleantheme = new() + { + PaletteLight = new PaletteLight() + { + Black = "#000", + White = "#fff", + Primary = "#2fa4e7ff", + PrimaryContrastText = "#d5edfaff", + Secondary = "#e9ecef", + SecondaryContrastText = "#495057ff", + Tertiary = "#dee2e6ff", + TertiaryContrastText = "#495057ff", + Info = "#033c73", + InfoContrastText = "#cdd8e3", + Success = "#73a839", + SuccessContrastText = "#e3eed7", + Warning = "#dd5600", + WarningContrastText = "#f8ddcc", + Error = "rgba(244,67,54,1)", + ErrorContrastText = "rgba(255,255,255,1)", + Dark = "#343a40", + DarkContrastText = "#ced4da", + TextPrimary = "#13425cff", + TextSecondary = "#5d5e60", + TextDisabled = "rgba(0,0,0,0.3764705882352941)", + ActionDefault = "rgba(0,0,0,0.5372549019607843)", + ActionDisabled = "rgba(0,0,0,0.25882352941176473)", + ActionDisabledBackground = "rgba(0,0,0,0.11764705882352941)", + Background = "rgba(255,255,255,1)", + BackgroundGray = "rgba(245,245,245,1)", + Surface = "rgba(255,255,255,1)", + DrawerBackground = "#e9ecefff", + DrawerText = "#495057ff", + DrawerIcon = "#e9ecef", + AppbarBackground = "#2fa4e7ff", + AppbarText = "#d5edfaff", + LinesDefault = "rgba(0,0,0,0.11764705882352941)", + LinesInputs = "rgba(189,189,189,1)", + TableLines = "rgba(224,224,224,1)", + TableStriped = "rgba(0,0,0,0.0196078431372549)", + TableHover = "rgba(0,0,0,0.0392156862745098)", + Divider = "rgba(224,224,224,1)", + DividerLight = "rgba(0,0,0,0.8)", + PrimaryDarken = "#13425c", + PrimaryLighten = "#d5edfa", + SecondaryDarken = "#5d5e60", + SecondaryLighten = "#fbfbfc", + TertiaryDarken = "rgba(73, 80, 87, 0.5)", + TertiaryLighten = "rgba(73, 80, 87, 0.5)", + InfoDarken = "#01182e", + InfoLighten = "#cdd8e3", + SuccessDarken = "#2e4317", + SuccessLighten = "#e3eed7", + WarningDarken = "#582200", + WarningLighten = "#f8ddcc", + ErrorDarken = "rgb(242,28,13)", + ErrorLighten = "rgb(246,96,85)", + DarkDarken = "rgb(46,46,46)", + DarkLighten = "rgb(87,87,87)", + HoverOpacity = 0.06, + RippleOpacity = 0.1, + RippleOpacitySecondary = 0.2, + GrayDefault = "#9E9E9E", + GrayLight = "#BDBDBD", + GrayLighter = "#E0E0E0", + GrayDark = "#757575", + GrayDarker = "#616161", + OverlayDark = "rgba(33,33,33,0.4980392156862745)", + OverlayLight = "rgba(255,255,255,0.4980392156862745)", + }, + PaletteDark = new PaletteDark() + { + Black = "#000", + White = "#fff", + Primary = "#2fa4e7ff", + PrimaryContrastText = "#d5edfaff", + Secondary = "#e9ecef", + SecondaryContrastText = "#2f2f30", + Tertiary = "rgba(222, 226, 230, 0.5)", + TertiaryContrastText = "#2b3035", + Info = "#026e8eff", + InfoContrastText = "#010c17", + Success = "#73a839", + SuccessContrastText = "#17220b", + Warning = "#dd5600", + WarningContrastText = "#2c1100", + Error = "rgba(244,67,54,1)", + ErrorContrastText = "rgba(255,255,255,1)", + Dark = "#343a40ff", + DarkContrastText = "#1a1d20", + TextPrimary = "#82c8f1", + TextSecondary = "#f2f4f5", + TextDisabled = "rgba(255,255,255,0.2)", + ActionDefault = "rgba(173,173,177,1)", + ActionDisabled = "rgba(255,255,255,0.25882352941176473)", + ActionDisabledBackground = "rgba(255,255,255,0.11764705882352941)", + Background = "rgba(50,51,61,1)", + BackgroundGray = "rgba(39,39,47,1)", + Surface = "#212529ff", + DrawerBackground = "#2f2f30ff", + DrawerText = "#e9ecef", + DrawerIcon = "#e9ecef", + AppbarBackground = "#2fa4e7ff", + AppbarText = "#d5edfaff", + LinesDefault = "rgba(255,255,255,0.11764705882352941)", + LinesInputs = "rgba(255,255,255,0.2980392156862745)", + TableLines = "rgba(255,255,255,0.11764705882352941)", + TableStriped = "rgba(255,255,255,0.2)", + Divider = "#ffffff73", + DividerLight = "#ffffff2e", + PrimaryDarken = "#82c8f1", + PrimaryLighten = "#09212e", + SecondaryDarken = "#f2f4f5", + SecondaryLighten = "#2f2f30", + TertiaryDarken = "rgba(222, 226, 230, 0.5)", + TertiaryLighten = "rgba(222, 226, 230, 0.5)", + InfoDarken = "#688aab", + InfoLighten = "#010c17", + SuccessDarken = "#abcb88", + SuccessLighten = "#17220b", + WarningDarken = "#eb9a66", + WarningLighten = "#2c1100", + ErrorDarken = "rgb(242,28,13)", + ErrorLighten = "rgb(246,96,85)", + DarkDarken = "rgb(23,23,28)", + DarkLighten = "rgb(56,56,67)", + }, + LayoutProperties = new LayoutProperties() + { + DefaultBorderRadius = "4px", + DrawerMiniWidthLeft = "56px", + DrawerMiniWidthRight = "56px", + DrawerWidthLeft = "240px", + DrawerWidthRight = "240px", + AppbarHeight = "64px", + }, + Typography = new Typography() + { + Default = new DefaultTypography + { + FontFamily = ["Roboto", "Helvetica", "Arial", "sans-serif"], + FontWeight = "400", + FontSize = ".875rem", + LineHeight = "1.43", + LetterSpacing = ".01071em", + TextTransform = "none", + }, + H1 = new H1Typography + { + FontWeight = "300", + FontSize = "6rem", + LineHeight = "1.167", + LetterSpacing = "-.01562em", + TextTransform = "none", + }, + H2 = new H2Typography + { + FontWeight = "300", + FontSize = "3.75rem", + LineHeight = "1.2", + LetterSpacing = "-.00833em", + TextTransform = "none", + }, + H3 = new H3Typography + { + FontWeight = "400", + FontSize = "3rem", + LineHeight = "1.167", + LetterSpacing = "0", + TextTransform = "none", + }, + H4 = new H4Typography + { + FontWeight = "400", + FontSize = "2.125rem", + LineHeight = "1.235", + LetterSpacing = ".00735em", + TextTransform = "none", + }, + H5 = new H5Typography + { + FontWeight = "400", + FontSize = "1.5rem", + LineHeight = "1.334", + LetterSpacing = "0", + TextTransform = "none", + }, + H6 = new H6Typography + { + FontWeight = "500", + FontSize = "1.25rem", + LineHeight = "1.6", + LetterSpacing = ".0075em", + TextTransform = "none", + }, + Subtitle1 = new Subtitle1Typography + { + FontWeight = "400", + FontSize = "1rem", + LineHeight = "1.75", + LetterSpacing = ".00938em", + TextTransform = "none", + }, + Subtitle2 = new Subtitle2Typography + { + FontWeight = "500", + FontSize = ".875rem", + LineHeight = "1.57", + LetterSpacing = ".00714em", + TextTransform = "none", + }, + Body1 = new Body1Typography + { + FontWeight = "400", + FontSize = "1rem", + LineHeight = "1.5", + LetterSpacing = ".00938em", + TextTransform = "none", + }, + Body2 = new Body2Typography + { + FontWeight = "400", + FontSize = ".875rem", + LineHeight = "1.43", + LetterSpacing = ".01071em", + TextTransform = "none", + }, + Button = new ButtonTypography + { + FontWeight = "500", + FontSize = ".875rem", + LineHeight = "1.75", + LetterSpacing = ".02857em", + TextTransform = "uppercase", + }, + Caption = new CaptionTypography + { + FontWeight = "400", + FontSize = ".75rem", + LineHeight = "1.66", + LetterSpacing = ".03333em", + TextTransform = "none", + }, + Overline = new OverlineTypography + { + FontWeight = "400", + FontSize = ".75rem", + LineHeight = "2.66", + LetterSpacing = ".08333em", + TextTransform = "none", + }, + }, + ZIndex = new ZIndex() + { + Drawer = 1100, + Popover = 1200, + AppBar = 1300, + Dialog = 1400, + Snackbar = 1500, + Tooltip = 1600, + }, + }; + } +} \ No newline at end of file diff --git a/WebApp/Components/Layout/MainLayout.razor b/WebApp/Components/Layout/MainLayout.razor index 42dc108..bfc0951 100644 --- a/WebApp/Components/Layout/MainLayout.razor +++ b/WebApp/Components/Layout/MainLayout.razor @@ -1,24 +1,32 @@ @inherits LayoutComponentBase +@inject IConfiguration Configuration - + + -
- - + + + + @Body + + + +@code { + bool _drawerOpen = true; -
- @*
- About -
*@ - -
- @Body -
-
-
+ void DrawerToggle() + { + _drawerOpen = !_drawerOpen; + } +}
An unhandled error has occurred. diff --git a/WebApp/Components/Layout/NavMenu.razor b/WebApp/Components/Layout/NavMenu.razor index 96c224e..a2bc497 100644 --- a/WebApp/Components/Layout/NavMenu.razor +++ b/WebApp/Components/Layout/NavMenu.razor @@ -1,43 +1,20 @@ - - - - - +@using WebApp.Models +@inject IConfiguration Configuration + + + TSA Chapter Organizer + @Configuration["ChapterSettings:Name"] + + Events + + Students + Event Ranking + + + Teams + Event Assignment + Schedule + + + \ No newline at end of file diff --git a/WebApp/Components/Layout/NavMenu.razor.css b/WebApp/Components/Layout/NavMenu.razor.css index 4e15395..bd112e5 100644 --- a/WebApp/Components/Layout/NavMenu.razor.css +++ b/WebApp/Components/Layout/NavMenu.razor.css @@ -3,7 +3,7 @@ cursor: pointer; width: 3.5rem; height: 2.5rem; - color: white; + color: blue; position: absolute; top: 0.5rem; right: 1rem; diff --git a/WebApp/Components/Pages/EventDefinitionPages/Events.razor b/WebApp/Components/Pages/EventDefinitionPages/Events.razor deleted file mode 100644 index b06f831..0000000 --- a/WebApp/Components/Pages/EventDefinitionPages/Events.razor +++ /dev/null @@ -1,87 +0,0 @@ -@using Core.Entities -@using Data -@using Microsoft.EntityFrameworkCore -@page "/events/descriptions" -@inject IConfiguration Configuration -@inject AppDbContext Context -@rendermode InteractiveServer - -TSA Events @Configuration["ChapterSettings:CompetitionYear"] - -

TSA Events @Configuration["ChapterSettings:CompetitionYear"]

- -@if (_events == null) -{ -

Loading...

-} -else -{ -
- @foreach (var evt in _events) - { -
- @if (evt.RegionalEvent) - { -
-
- Regional Event -
-
- } -
-
-
@evt.Name
-
-
- @if (evt.EventFormat is EventFormat.Team) - { - @evt.EventFormat
Size: @evt.TeamSize - } - else - { - - @evt.EventFormat - - } - -
-
- Eligibility: @evt.Eligibility -
-
- Effort: @evt.LevelOfEffort -
-
- Activity: @evt.SemifinalistActivity -
-
-
-
@evt.Description
- @if (!string.IsNullOrEmpty(evt.Theme)) - { -
-
Theme for 2025-26:
-
@evt.Theme
-
- } - - @if (!string.IsNullOrEmpty(evt.Documentation)) - { -
-
Materials:
-
@evt.Documentation
-
- } -
-
- } -
-} -@code { - private EventDefinition[]? _events = null; - - protected override async Task OnInitializedAsync() - { - _events = await Context.Events.OrderBy(e => e.Name).Where(e => e.Name != "Chapter Team").ToArrayAsync(); - } -} diff --git a/WebApp/Components/Pages/EventDefinitionPages/Index.razor b/WebApp/Components/Pages/EventDefinitionPages/Index.razor index 7638593..01af8b4 100644 --- a/WebApp/Components/Pages/EventDefinitionPages/Index.razor +++ b/WebApp/Components/Pages/EventDefinitionPages/Index.razor @@ -2,27 +2,25 @@ @using Microsoft.EntityFrameworkCore @inject AppDbContext Context - Events - TSA Chapter Organizer Events Create New +Printable Descriptions - - + - - + [@context.Item.MinTeamSize - @context.Item.MaxTeamSize] - + @context.Item.MaxTeamCountState @@ -50,27 +48,6 @@ -@* - - - - @* - - - - - - - - - - - - - - - *@ - @code { MudDataGrid _dataGrid = null!; diff --git a/WebApp/Components/Pages/EventDefinitionPages/Printout.razor b/WebApp/Components/Pages/EventDefinitionPages/Printout.razor new file mode 100644 index 0000000..dfaff5e --- /dev/null +++ b/WebApp/Components/Pages/EventDefinitionPages/Printout.razor @@ -0,0 +1,178 @@ +@using Microsoft.EntityFrameworkCore +@page "/events/printout" +@inject IConfiguration Configuration +@inject AppDbContext Context + +TSA Events @Configuration["ChapterSettings:CompetitionYear"] + +TSA Events @Configuration["ChapterSettings:CompetitionYear"] +Yearly theme: Unity Through Community + +@if (_events == null) +{ +

Loading...

+} +else +{ + + @foreach (var evt in _events) + { + + + + + + @evt.Name + + @if (evt.RegionalEvent) + { + + Regional Event + + } + + + + + + @if (evt.EventFormat is EventFormat.Team) + { + @evt.EventFormat +
+

Size: @evt.TeamSize

+ } + else + { + @evt.EventFormat + } +
+ +
+ + Eligibility: @evt.Eligibility + + + Effort: @evt.LevelOfEffort + + + Activity: @evt.SemifinalistActivity + + + + @evt.Description + + @if (!string.IsNullOrEmpty(evt.Theme)) + { + + + Theme for 2025-26: + + + + @evt.Theme + + } + + @if (!string.IsNullOrEmpty(evt.Documentation)) + { + + + Materials: + + + + @evt.Documentation + + } + +
+
+ + } +
+ + + @foreach (var evt in _events) + { + + + + + + @evt.Name + + @if (evt.RegionalEvent) + { + + Regional Event + + } + + + + + + @if (evt.EventFormat is EventFormat.Team) + { + @evt.EventFormat +
+

Size: @evt.TeamSize

+ } + else + { + @evt.EventFormat + } +
+ +
+ + Eligibility: @evt.Eligibility + + + Effort: @evt.LevelOfEffort + + + Activity: @evt.SemifinalistActivity + + + + @evt.Description + + @if (!string.IsNullOrEmpty(evt.Theme)) + { + + + Theme for 2025-26: + + + + @evt.Theme + + } + + @if (!string.IsNullOrEmpty(evt.Documentation)) + { + + + Materials: + + + + @evt.Documentation + + } + +
+
+ + } +
+} +@code { + private EventDefinition[]? _events; + + protected override async Task OnInitializedAsync() + { + _events = await Context.Events.OrderBy(e => e.Name).ToArrayAsync(); + } +} diff --git a/WebApp/Components/Pages/SchedulePages/Index.razor b/WebApp/Components/Pages/SchedulePages/Index.razor new file mode 100644 index 0000000..b33e032 --- /dev/null +++ b/WebApp/Components/Pages/SchedulePages/Index.razor @@ -0,0 +1,5 @@ +

Index

+ +@code { + +} diff --git a/WebApp/Components/Pages/StudentPages/EventRanking.razor b/WebApp/Components/Pages/StudentPages/EventRanking.razor index 0ad76ef..c4552b8 100644 --- a/WebApp/Components/Pages/StudentPages/EventRanking.razor +++ b/WebApp/Components/Pages/StudentPages/EventRanking.razor @@ -1,16 +1,20 @@ @using Microsoft.EntityFrameworkCore +@using WebApp.Models @page "/students/event-ranking" @inject AppDbContext Context @rendermode InteractiveServer Student Event Ranks - TSA Chapter Organizer + Student Event Ranks + @if (_students == null) {

Loading...

} else { + @@ -23,6 +27,10 @@ else 4th 5th 6th + 7th + 8th + 9th + 10th Warnings @@ -32,7 +40,7 @@ else @context.Grade @context.TsaYear - @for (var i = 1; i <= 6; i++) + @for (var i = 1; i <= 10; i++) { var st = context.EventRankings.FirstOrDefault(e => e.Rank == i); @@ -40,8 +48,8 @@ else { @st.EventDefinition.ShortName  @if(st.EventDefinition.EventFormat == EventFormat.Individual) { } - @if(st.EventDefinition.RegionalEvent) { } - @if(st.EventDefinition.OnSiteActivity) { } + @if(st.EventDefinition.RegionalEvent) {} + @if(st.EventDefinition.OnSiteActivity) {} } diff --git a/WebApp/Components/Pages/StudentPages/EventRankingEdit.razor b/WebApp/Components/Pages/StudentPages/EventRankingEdit.razor index 7e55395..9aef4ca 100644 --- a/WebApp/Components/Pages/StudentPages/EventRankingEdit.razor +++ b/WebApp/Components/Pages/StudentPages/EventRankingEdit.razor @@ -7,7 +7,7 @@ Student Event Ranks - TSA Chapter Organizer -Student Event Ranks + Student Event Ranks
@if (_student == null) diff --git a/WebApp/Components/Pages/StudentPages/Index.razor b/WebApp/Components/Pages/StudentPages/Index.razor index db584e7..3752f42 100644 --- a/WebApp/Components/Pages/StudentPages/Index.razor +++ b/WebApp/Components/Pages/StudentPages/Index.razor @@ -8,14 +8,14 @@ Students Create New -Event Rankings +Event Rankings @* *@ - + - @context.Item.Name + @context.Item.LastNameFirstName @if (context.Item.OfficerRole != null) { @context.Item.OfficerRole @@ -56,7 +56,9 @@ private async Task> ServerReload(GridState state) { - var query = Context.Students.Where(state.FilterDefinitions).OrderBy(state.SortDefinitions); + var query = + Context.Students.OrderBy(e => e.LastName) + .Where(state.FilterDefinitions).OrderBy(state.SortDefinitions); var totalItems = await query.CountAsync(); var pagedData = await query.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArrayAsync(); diff --git a/WebApp/Components/Pages/TeamPages/Assignment.razor b/WebApp/Components/Pages/TeamPages/Assignment.razor index 8a7ecf9..c55adda 100644 --- a/WebApp/Components/Pages/TeamPages/Assignment.razor +++ b/WebApp/Components/Pages/TeamPages/Assignment.razor @@ -10,11 +10,108 @@ Assignment + + Optimized team assignments based on the student event rankings + Edit Student Event Rankings + -Optimized team assignments based on the student event rankings + + + + + + + + + + + + + + + + + + + Event Count + + + + + + + + + + LOE + + + + + + + + Assignment Requirements + + + + + + + + @item.Student.FirstName + @item.EventDefinition.ShortName + @if (item.Requirement == Requirement.Include) + { + + } + @if (item.Requirement == Requirement.Exclude) + { + + } + + + + + + Two Team Events + + + + + + + @item.ShortName + + + + + Omitted Events + + + + + + + @item.ShortName + + + + + Solve + - Students @@ -50,7 +147,7 @@ - + @{ var allStudentEvents = context.Student.EventRankings @@ -68,7 +165,7 @@ var eventRank = context.Student.EventRankings.Find(er => er.EventDefinition == e)?.Rank; var isAssigned = context.Events.Contains(e); - var color = AppIcons.RankedEvent(eventRank ?? 0); + var color = AppIcons.RankedEventColor(eventRank ?? 0); var style = string.Empty; if (isAssigned) @@ -90,7 +187,8 @@ } - @e.ShortName + @e.ShortName  + @AppIcons.EventAttributes(e) @{ var isIncluded = _assignmentRequirements .Find(ar => @@ -134,152 +232,106 @@ } } - + - Teams - - - - - - - Team - R, # [LB-UB] - - - @{ - var thresholds = _eventAssignmentThresholds.First(e => e.Event == context.Event); - } - @context.Event.Name - @thresholds.StudentRankingCount, @thresholds.TeamCount × [@thresholds.LowerBound-@thresholds.UpperBound] - - - - - - @foreach (var student in - context.Students - .OrderBy(e => - e.EventRankings - .Find(er => er.EventDefinition == context.Event)?.Rank ?? 10) - .ThenBy(s => s.Grade + s.TsaYear)) + TimeSlots + + + + + + + Team + R, # [LB-UB] + + + @{ + var thresholds = _eventAssignmentThresholds.First(e => e.Event == context.Event); + } + + @context.Event.Name  + @AppIcons.EventEffort(context.Event) @AppIcons.EventAttributes(context.Event) + + + + @thresholds.StudentRankingCount, [@thresholds.LowerBound-@thresholds.UpperBound] × @thresholds.TeamCount + + @if (_eventTwoTeams.Contains(context.Event)) { - var eventRank = - student.EventRankings - .Find(e => e.EventDefinition == context.Event)?.Rank; - var color = AppIcons.RankedEvent(eventRank ?? 0); - - - @student.FirstName - + } + else if (thresholds.TeamCount != 2 && context.Event.EventFormat == EventFormat.Team) + { + + } + + + + + + + + @foreach (var student in + context.Students + .OrderBy(e => + e.EventRankings + .Find(er => er.EventDefinition == context.Event)?.Rank ?? 10) + .ThenBy(s => s.Grade + s.TsaYear)) + { + var eventRank = + student.EventRankings + .Find(e => e.EventDefinition == context.Event)?.Rank; + var color = AppIcons.RankedEventColor(eventRank ?? 0); + + + @student.FirstName + + } - - - - - Solution status: @_solutionStatus - - - Loading... - - - + + + + + Solution status: @_solutionStatus + + + Loading... + + + - - - - - - - - - - - - - - - - - - - Event Count - - - - - - - - - - LOE - - - - - - - - Assignment Requirements - - - - Student - Event - - - - - - @item.Student.FirstName - - @item.EventDefinition.ShortName - @if (item.Requirement == Requirement.Include) - { - - } - @if (item.Requirement == Requirement.Exclude) - { - - } - - - - - - Solve - - -Edit Student Event Rankings + +Save + @code { public bool TestSwitch { get; set; } = false; - private readonly AssignmentParameters _parameters = new () {LimitTeamsToOne = false}; + private readonly AssignmentParameters _parameters = new() { RequireOnSite = false, RequireRegional = false }; private List? _events; private List? _students; private List _eventAssignmentThresholds = []; MudTable _teamData; + private Team[] _teams = []; MudTable _statisticData; private List _statistics = []; MudTable _assignmentRequirementData; private List _assignmentRequirements = []; + MudTable _eventTwoTeamData; + private List _eventTwoTeams = []; + MudTable _eventOmittedData; + private List _eventOmitted = []; private string _solutionStatus = string.Empty; @@ -302,7 +354,7 @@ private async Task AddTeam() { - //Context.Teams.Add(Team); + //Context.TimeSlots.Add(Team); await Context.SaveChangesAsync(); //NavigationManager.NavigateTo("/teams"); } @@ -320,6 +372,10 @@ { eventAssignment.AddAssignmentRequirement(requirement); } + eventAssignment.RemoveEvents(_eventOmitted); + + eventAssignment.AllowTwoTeams(_eventTwoTeams); + var solution = await eventAssignment.Solve(); _solutionStatus = solution.Status; _statistics = @@ -330,7 +386,9 @@ await _statisticData.ReloadServerData(); _isSolving = false; await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found - return new TableData { Items = solution.Teams }; + _teams = solution.Teams; + + return new TableData { Items = _teams }; } private async Task> ReloadStatistics(TableState arg1, CancellationToken arg2) @@ -343,6 +401,17 @@ return new TableData { Items = _assignmentRequirements }; } + private async Task> ReloadEventTwoTeam(TableState arg1, CancellationToken arg2) + { + return new TableData { Items = _eventTwoTeams }; + } + + + private async Task> ReloadOmittedEvents(TableState arg1, CancellationToken arg2) + { + return new TableData { Items = _eventOmitted }; + } + private void RequireEvent(EventDefinition evt, Student student, Requirement requirement) { _assignmentRequirements.Add(new AssignmentRequirement(evt, student, requirement)); @@ -362,4 +431,39 @@ _assignmentRequirements.Remove(assignmentRequirement); _assignmentRequirementData.ReloadServerData(); } + + private void AddTwoTeam(EventDefinition evt) + { + _eventTwoTeams.Add(evt); + _eventTwoTeamData.ReloadServerData(); + } + + private void RemoveTwoTeam(EventDefinition evt) + { + _eventTwoTeams.Remove(evt); + _eventTwoTeamData.ReloadServerData(); + } + + + private void AddOmitted(EventDefinition evt) + { + _eventOmitted.Add(evt); + _eventOmittedData.ReloadServerData(); + } + + private void RemoveOmitted(EventDefinition evt) + { + _eventOmitted.Remove(evt); + _eventOmittedData.ReloadServerData(); + } + + public async Task SaveTeams() + { + var teams = await Context.Teams.ExecuteDeleteAsync(); + + await Context.Teams.AddRangeAsync(_teams); + await Context.SaveChangesAsync(); + + NavigationManager.NavigateTo("/teams"); + } } \ No newline at end of file diff --git a/WebApp/Components/Pages/TeamPages/Create.razor b/WebApp/Components/Pages/TeamPages/Create.razor index 7396a76..af76146 100644 --- a/WebApp/Components/Pages/TeamPages/Create.razor +++ b/WebApp/Components/Pages/TeamPages/Create.razor @@ -21,7 +21,7 @@ } - + @@ -46,7 +46,9 @@ private async Task AddTeam() { + Team.TeamId = Team.Event.Name; Context.Teams.Add(Team); + await Context.SaveChangesAsync(); NavigationManager.NavigateTo("/teams"); } diff --git a/WebApp/Components/Pages/TeamPages/Delete.razor b/WebApp/Components/Pages/TeamPages/Delete.razor new file mode 100644 index 0000000..6d7b5b8 --- /dev/null +++ b/WebApp/Components/Pages/TeamPages/Delete.razor @@ -0,0 +1,60 @@ +@page "/teams/delete" +@using Microsoft.EntityFrameworkCore +@inject AppDbContext context +@inject NavigationManager NavigationManager + +Delete Team + +

Delete

+ +

Are you sure you want to delete this?

+
+

Team

+
+ @if (team is null) + { +

Loading...

+ } + else { +
+
Team
+
@team.Event.Name
+
+
+
Students
+
@string.Join(",", team.Students.Select(e => e.Name))
+
+ + + | + Back to List + + } +
+ +@code { + private Team? team; + + [SupplyParameterFromQuery] + private int Id { get; set; } + + protected override async Task OnInitializedAsync() + { + team = await context.Teams + .Include(e => e.Event) + .Include(e => e.Students) + .FirstOrDefaultAsync(m => m.Id == Id); + + if (team is null) + { + NavigationManager.NavigateTo("notfound"); + } + } + + private async Task DeleteTeam() + { + context.Teams.Remove(team!); + await context.SaveChangesAsync(); + NavigationManager.NavigateTo("/teams"); + } +} diff --git a/WebApp/Components/Pages/TeamPages/Edit.razor b/WebApp/Components/Pages/TeamPages/Edit.razor new file mode 100644 index 0000000..4965b93 --- /dev/null +++ b/WebApp/Components/Pages/TeamPages/Edit.razor @@ -0,0 +1,105 @@ +@page "/teams/edit" +@using Microsoft.EntityFrameworkCore +@inject AppDbContext Context +@inject NavigationManager NavigationManager + +Edit Team - TSA Chapter Organizer + +Edit +Team@(Team == null ? "" : $" ({Team.Event.Name} #{Team.Number})") + +@if (Team is null) +{ +

Loading...

+} +else +{ + + + + + + + + @foreach (var student in _students.OrderBy(e => e.FirstName)) + { + @student.Name + } + + + + + + Back + Save + +} + +@code { + [SupplyParameterFromQuery] + private int Id { get; set; } + + [SupplyParameterFromForm] + private Team? Team { get; set; } + + private IEnumerable _selectedStudents = new HashSet(); + private List _students = []; + + protected override async Task OnInitializedAsync() + { + Team ??= await Context.Teams + .Include(e => e.Event) + .Include(e => e.Students) + .FirstOrDefaultAsync(m => m.Id == Id); + _students = await Context.Students.ToListAsync(); + foreach (var s in Team.Students) + { + ((HashSet)_selectedStudents).Add(s); + } + + if (Team is null) + { + NavigationManager.NavigateTo("notfound"); + } + } + + // To protect from overposting attacks, enable the specific properties you want to bind to. + // For more information, see https://learn.microsoft.com/aspnet/core/blazor/forms/#mitigate-overposting-attacks. + private async Task UpdateTeam() + { + //Context.Attach(Team!).Entity = EntityState.Modified; + Team.Students.Clear(); + foreach (var s in _selectedStudents) + { + Team.Students.Add(s); + } + + try + { + await Context.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + if (!TeamExists(Team!.Id)) + { + NavigationManager.NavigateTo("notfound"); + } + else + { + throw; + } + } + + NavigationManager.NavigateTo("/teams"); + } + + private bool TeamExists(int id) + { + return Context.Teams.Any(e => e.Id == id); + } +} diff --git a/WebApp/Components/Pages/TeamPages/Index.razor b/WebApp/Components/Pages/TeamPages/Index.razor index a7a800c..ecaca2c 100644 --- a/WebApp/Components/Pages/TeamPages/Index.razor +++ b/WebApp/Components/Pages/TeamPages/Index.razor @@ -3,26 +3,38 @@ @using WebApp.Models @inject AppDbContext Context -Teams +TimeSlots Teams Create New Assignment +Printout - + - - - @* + + + - @context.Item.Name - @if (context.Item.OfficerRole != null) + @foreach (var student in + context.Item.Students + .OrderBy(e => + e.EventRankings + .Find(er => er.EventDefinition == context.Item.Event)?.Rank ?? 10) + .ThenBy(s => s.Grade + s.TsaYear)) { - @context.Item.OfficerRole + var eventRank = + student.EventRankings + .Find(e => e.EventDefinition == context.Item.Event)?.Rank; + var color = AppIcons.RankedEventColor(eventRank ?? 0); + + + @student.FirstName + } - *@ + @* @context.Item.Grade (@context.Item.TsaYear) @@ -60,6 +72,9 @@ = Context.Teams .Include(e => e.Event) .Include(e => e.Students) + .ThenInclude(e => e.EventRankings) + .OrderBy(e => e.Event.Name) + .ThenBy(e => e.Number) .Where(state.FilterDefinitions) .OrderBy(state.SortDefinitions); diff --git a/WebApp/Components/Pages/TeamPages/Printout.razor b/WebApp/Components/Pages/TeamPages/Printout.razor new file mode 100644 index 0000000..6b3f39a --- /dev/null +++ b/WebApp/Components/Pages/TeamPages/Printout.razor @@ -0,0 +1,307 @@ +@using Microsoft.EntityFrameworkCore +@using WebApp.Models +@page "/teams/printout" +@inject IConfiguration Configuration +@inject AppDbContext Context + +@Configuration["ChapterSettings:Shortname"] TSA Teams @Configuration["ChapterSettings:CompetitionYear"] + +@Configuration["ChapterSettings:Shortname"] TSA Teams @Configuration["ChapterSettings:CompetitionYear"] +Yearly theme: Unity Through Community + +@if (_teams == null) +{ +

Loading...

+} +else +{ + + + + + Team + + @for (var i = 0; i <= _maxTeamSize; i++) + { + + } + + + + + @context.ToString() + + + @AppIcons.EventEffort(context.Event) + @AppIcons.EventAttributes(context.Event) + + + @{ + var students + = context.Students + .OrderBy(s => s.EventRankings.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue) + .ThenByDescending(e => e.Grade) + .ThenBy(e => e.FirstName) + .ToArray(); + } + + @for (var i = 0; i <= _maxTeamSize; i++) + { + var student = i < students.Length ? students[i] : null; + if (student != null) + { + var rank = student.EventRankings + .Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue; + + + @student.Name + + } + else + { + + } + } + + @if (context.Event.EventFormat == EventFormat.Team) + { + @if (context.Students.Count < context.Event.MinTeamSize) + { + Min Team Size: @context.Event.MinTeamSize + } + + @if (context.Students.Count > context.Event.MaxTeamSize) + { + Max Team Size: @context.Event.MaxTeamSize + } + } + else if (context.Event.EventFormat == EventFormat.Individual + && context.Students.Count > context.Event.MaxTeamCountState) + { + Max Team Count State: @context.Event.MaxTeamCountState + } + + + + + + + + + + + Student + Effort + @for (var i = 0; i <= 5; i++) + { + + } + + + + + @context.Name + @context.Teams.Sum(e => e.Event.LevelOfEffort) + @{ + var teams = context.Teams + .OrderBy(e => + context.EventRankings.Find(ser => ser.EventDefinition == e.Event)?.Rank ?? int.MaxValue + ).ToArray(); + } + @for (var i = 0; i <= 5; i++) + { + var team = i < teams.Length ? teams[i] : null; + + @if (team != null) + { + var rank = context.EventRankings + .Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue; + + @team.ToString() + @AppIcons.EventEffort(team.Event) + @AppIcons.EventAttributes(team.Event) + + @if (rank == int.MaxValue) + { + + } + + } + else + { + + } + + } + + @if (!context.Teams.Select(e => e.Event).Any(re => re.OnSiteActivity)) + { + No On-Site Activity + } + @if (!context.Teams.Select(e => e.Event).Any(re => re.RegionalEvent)) + { + No Regional Event + } + + + + + + + + + + + + Student + Grade, TSA Year + Level of Effort + + + + @context.Name + @AppIcons.GetOrdinal(context.Grade), @context.TsaYear + @context.Teams.Sum(e => e.Event.LevelOfEffort) + @if (!context.Teams.Select(e => e.Event).Any(re => re.OnSiteActivity)) + { + No On-Site Activity + } + @if (!context.Teams.Select(e => e.Event).Any(re => re.RegionalEvent)) + { + No Regional Event + } + + + + + + + @{ var rank = context.EventRankings + .Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue; } + + @team.ToString() + @AppIcons.EventEffort(team.Event) + @AppIcons.EventAttributes(team.Event) + + @if (rank == int.MaxValue) + { + + } + + @string.Join(", ", team.Students.Where(e => e != context).Select(e => e.FirstName)) + + + + + + + + + + + @foreach (var studentForEvents in _students) + { + + @studentForEvents.Name + @foreach (var team in studentForEvents.Teams) + { + + + + + + + @team.ToString() + + + + @if (team.Event.RegionalEvent) + { + + Regional Event + + } + + + + + @team.Event.EventFormat + + + + + Effort: @AppIcons.LevelOfEffortIcon(@team.Event.LevelOfEffort) + + + Activity: @team.Event.SemifinalistActivity + + @if (team.Event.EventFormat == EventFormat.Team) + { + + + Team Members: @string.Join(", ", team.Students.OrderByDescending(e => e.Grade + e.TsaYear).Select(e => e.FirstName)) + + + } + + @team.Event.Description + + @if (!string.IsNullOrEmpty(team.Event.Theme)) + { + + + Theme for 2025-26: + + + + @team.Event.Theme + + } + + @if (!string.IsNullOrEmpty(team.Event.Documentation)) + { + + + Materials: + + + + @team.Event.Documentation + + } + + + + + } + + } + +} +@code { + private Team[]? _teams; + private int _maxTeamSize; + private Student[]? _students; + + protected override async Task OnInitializedAsync() + { + _teams + = await Context.Teams + .Include(e => e.Event) + .Include(e => e.Students) + .OrderBy(e => e.Event.Name) + .ThenBy(e => e.Number) + .ToArrayAsync(); + + _maxTeamSize = _teams.Max(t => t.Students.Count); + _students = + await Context.Students + .Include(e => e.Teams) + .ThenInclude(e => e.Captain) + .Include(e => e.EventRankings) + .ThenInclude(e => e.EventDefinition) + .OrderBy(e => e.FirstName).ToArrayAsync(); + } +} diff --git a/WebApp/Components/Pages/TeamPages/Scheduler.razor b/WebApp/Components/Pages/TeamPages/Scheduler.razor new file mode 100644 index 0000000..0c70add --- /dev/null +++ b/WebApp/Components/Pages/TeamPages/Scheduler.razor @@ -0,0 +1,112 @@ +@using Core.Calculation +@using Microsoft.EntityFrameworkCore +@page "/teams/scheduler" +@inject IConfiguration Configuration +@inject AppDbContext Context + +@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"] + +@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"] + + +Time Slots + + + + + + + @foreach (var t in context) + { + + @t.ToString() - + @string.Join(", ", t.Students) + + } + @foreach (var overlap in TeamSchedulerSolution.GetStudentTeamOverlaps(context)) + { + + @string.Join(", ", overlap.Item1) + + } + + + + + +@code { + private Team[]? _teams; + private Student[]? _students; + MudTable _solutionData; + private TeamSchedulerSolution _solution; + + protected override async Task OnInitializedAsync() + { + _teams + = await Context.Teams + .Include(e => e.Event) + .Include(e => e.Students) + .OrderBy(e => e.Event.Name) + .ThenBy(e => e.Number) + .ToArrayAsync(); + + _students = + await Context.Students + .Include(e => e.Teams) + .ThenInclude(e => e.Captain) + .Include(e => e.EventRankings) + .ThenInclude(e => e.EventDefinition) + .OrderBy(e => e.FirstName).ToArrayAsync(); + } + + private async Task> SolveSchedule(TableState arg1, CancellationToken arg2) + { + //_isSolving = true; + + var scheduleOptions = + new TeamSchedulerOptions( + timeSlots: 4, + mustIncludeEvents: + [ + // "Medical Technology", "Electrical Applications" , "RegionalTeam", + // ,"Dragster", "Flight" + ], + extended: + [ + // "Invention", "Construction Challenge", "Mechanical", "Mass", "Micro" + //"STEM" + //"Community", "Vlogging"// "Microcontroller" + ], + omittedEvents: + [ + // "Vlogging", "Junior", "Community Service Video", "Digital Photography", + // "STEM" + + //"Leadership",// "Electrical", //"Construction" + // "Forensic", + //"CAD" + //"I&I Team 1", "I&I Team 2"//, "Website Design", + ], + absentStudents: + [ + ] + ); + + var mustIncludeTeams = _teams; + mustIncludeTeams = mustIncludeTeams.Where(t => t.Event.EventFormat == EventFormat.Team).ToArray(); + + var teamScheduler = new TeamScheduler(mustIncludeTeams, scheduleOptions.TimeSlots); + + // teamScheduler + // .ScheduleSeparate( + // _teams.First(e => e.Event.Name.Contains("Data Science")), + // _teams.First(e => e.Event.Name.Contains("Microcontroller Design")) + // ); + + _solution = teamScheduler.Solve(); + + await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found + + return new TableData { Items = _solution.TimeSlots}; + } +} diff --git a/WebApp/Models/AppIcons.cs b/WebApp/Models/AppIcons.cs index 4d186db..9a0ffe9 100644 --- a/WebApp/Models/AppIcons.cs +++ b/WebApp/Models/AppIcons.cs @@ -5,34 +5,62 @@ namespace WebApp.Models { public static class AppIcons { - private const string Prefix = "@Icons.Material.Filled."; - - public static string LevelOfEffortIcon(int loe) + public static string EventRank = Icons.Material.Filled.AddChart; + public static string Teams = Icons.Material.Filled.Group; + public static string Student = Icons.Material.Filled.Person; + public static string TeamAssignment = Icons.Material.Filled.GroupAdd; + public static string Events = Icons.Material.Filled.Dashboard; + public static string LevelOfEffortIcon(int? loe) { return loe switch { - 1 => MudBlazor.Icons.Material.Filled.StarBorder, - 2 => MudBlazor.Icons.Material.Filled.StarHalf, - 3 => MudBlazor.Icons.Material.Filled.Star, - _ => MudBlazor.Icons.Material.Filled.QuestionMark + 1 => "①", + 2 => "②", + 3 => "③", + _ => Icons.Material.Filled.QuestionMark }; } + + public static string OnSiteActivity = "ⓐ"; + public static string RegionalEvent = "ⓡ"; + public static string IndividualEvent = "ⓘ"; + public static string QuestionMark = "❔"; + + public static string EventEffort(EventDefinition eventDefinition) + { + return LevelOfEffortIcon(eventDefinition.LevelOfEffort); + } + + public static string EventAttributes(EventDefinition eventDefinition) + { + var v = new List(); + + if (eventDefinition.EventFormat == EventFormat.Individual) + v.Add(IndividualEvent); + if (eventDefinition.OnSiteActivity) + v.Add(OnSiteActivity); + if (eventDefinition.RegionalEvent) + v.Add(RegionalEvent); + + return string.Join(" ", v); + } + public static string OfficerRoleIcon(OfficerRole officerRole) { return officerRole switch { - OfficerRole.President => MudBlazor.Icons.Material.Filled.Gavel, - OfficerRole.VicePresident => MudBlazor.Icons.Material.Filled.StarBorderPurple500, - OfficerRole.Secretary => MudBlazor.Icons.Material.Filled.Draw, - OfficerRole.Treasurer => MudBlazor.Icons.Material.Filled.Key, - OfficerRole.Reporter => MudBlazor.Icons.Material.Filled.Mic, - OfficerRole.SergeantAtArms => MudBlazor.Icons.Material.Filled.Handshake, + OfficerRole.President => Icons.Material.Filled.Gavel, + OfficerRole.VicePresident => Icons.Material.Filled.StarBorderPurple500, + OfficerRole.Secretary => Icons.Material.Filled.Draw, + OfficerRole.Treasurer => Icons.Material.Filled.Key, + OfficerRole.Reporter => Icons.Material.Filled.Mic, + OfficerRole.SergeantAtArms => Icons.Material.Filled.Handshake, _ => throw new ArgumentOutOfRangeException(nameof(officerRole), officerRole, null) }; } - public static string RankedEvent(int rank) + public static string RankedEventColor(int rank) { return rank switch { @@ -42,16 +70,37 @@ namespace WebApp.Models 4 => "#ffe599", 5 => "#fff2cc", 6 => "#fffaea", + 7 => "#fffefa", + 8 => "#fffefc", + 9 => "#fffffd", + 10 => "#fffffe", _ => "#ddd" }; } - /* - * #dd7e6b; - #ea9999; - #f9cb9c; - #ffe599; - #fff2cc; - #fffaea; - */ + + public static string GetOrdinal(int num) + { + if (num <= 0) return num.ToString(); + + switch (num % 100) + { + case 11: + case 12: + case 13: + return num + "th"; + } + + switch (num % 10) + { + case 1: + return num + "st"; + case 2: + return num + "nd"; + case 3: + return num + "rd"; + default: + return num + "th"; + } + } } } diff --git a/WebApp/WebApp.csproj b/WebApp/WebApp.csproj index 17c8e84..130d28b 100644 --- a/WebApp/WebApp.csproj +++ b/WebApp/WebApp.csproj @@ -27,8 +27,4 @@ - - - - diff --git a/WebApp/wwwroot/app.css b/WebApp/wwwroot/app.css index 42d1b35..bc91084 100644 --- a/WebApp/wwwroot/app.css +++ b/WebApp/wwwroot/app.css @@ -1,4 +1,4 @@ -html, body { +/*html, body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; } @@ -48,7 +48,7 @@ h1:focus { .darker-border-checkbox.form-check-input { border-color: #929292; -} +}*/ @media print { @@ -56,20 +56,24 @@ h1:focus { max-width: 1200px; }*/ - main { + div.mud-main-content > div { font-size: 11px; margin: 30pt; color: #000; background-color: #fff; } - body .sidebar, main > div.top-row { - display: none; + .no-print { + display: none !important; } .nobrk { break-inside: avoid; } + + .pagebreak { + page-break-after: always; + } } .ranked-event-column > div:only-child{