Compare commits

..
10 Commits
Author SHA1 Message Date
poprhythm ea21406309 Meeting schedule updates
including copy to clipboard
2025-10-27 10:13:54 -04:00
poprhythm 2119de05a8 Update formating for event ranking 2025-10-22 10:11:07 -04:00
poprhythm 5708d846d1 Fix for team comparison in UnassignedStudentScheduler 2025-10-22 09:42:44 -04:00
poprhythm d777b4f67d Update the icons on Ranking and Assignment 2025-10-20 12:54:48 -04:00
poprhythm c42cc43399 Scheduler refinements 2025-10-20 12:03:46 -04:00
poprhythm dd8921f6fe Fix broken tests 2025-10-19 13:34:34 -04:00
poprhythm 054166d7af Redo routes for meeting schedule 2025-10-19 10:05:18 -04:00
poprhythm 349ec09cd7 Scheduler improvements 2025-10-19 10:03:26 -04:00
poprhythm 0ff45f9310 Docker and EF Sqlite adjustments 2025-10-17 14:28:10 -04:00
poprhythm 3964a61794 Team schedule improvements 2025-10-17 07:58:14 -04:00
29 changed files with 521 additions and 366 deletions
+3 -3
View File
@@ -14,9 +14,9 @@ public class TeamScheduler
private readonly List<Tuple<int,int>> _scheduleSeparateTeams = []; private readonly List<Tuple<int,int>> _scheduleSeparateTeams = [];
public TeamScheduler(Team[] teams, int numTimeSlots) public TeamScheduler(IEnumerable<Team> teams, int numTimeSlots)
{ {
_teamObjects = teams; _teamObjects = teams.ToArray();
_studentObjects = teams.SelectMany(t => t.Students).Distinct().ToList(); _studentObjects = teams.SelectMany(t => t.Students).Distinct().ToList();
_students = Enumerable.Range(0, _studentObjects.Count).ToArray(); _students = Enumerable.Range(0, _studentObjects.Count).ToArray();
@@ -31,7 +31,7 @@ public class TeamScheduler
_scheduleSeparateTeams.Add(Tuple.Create(one,two)); _scheduleSeparateTeams.Add(Tuple.Create(one,two));
} }
public static TeamScheduler CreateInstance(Team[] teams, int numTimeSlots) public static TeamScheduler CreateInstance(IEnumerable<Team> teams, int numTimeSlots)
{ {
return new TeamScheduler(teams, numTimeSlots); return new TeamScheduler(teams, numTimeSlots);
} }
@@ -41,4 +41,11 @@ public class TeamSchedulerSolution(
).ToArray(); ).ToArray();
} }
public Team[] StudentUnassignedTeams(Student student)
{
var meetingTeams = TimeSlots.SelectMany(t => t);
return
student.Teams.Where(e => !meetingTeams.Contains(e)).ToArray();
}
} }
+15 -13
View File
@@ -12,7 +12,7 @@ public class UnassignedStudentScheduler
{ {
_teams = teams; _teams = teams;
_students = teams.SelectMany(t => t.Students).Distinct().ToArray(); _students = teams.SelectMany(t => t.Students).Distinct().ToArray();
_timeSlots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToArray()).ToArray(); _timeSlots = timeslots.Select(ts => ts.Select(t => t.Clone()).ToList()).ToArray<IList<Team>>();
} }
public static IEnumerable<Student> UnassignedStudents(IList<Student> students, IList<Team> timeSlot) public static IEnumerable<Student> UnassignedStudents(IList<Student> students, IList<Team> timeSlot)
=> students.Where(s => !timeSlot.SelectMany(t => t.Students).Contains(s)); => students.Where(s => !timeSlot.SelectMany(t => t.Students).Contains(s));
@@ -20,9 +20,9 @@ public class UnassignedStudentScheduler
public static IEnumerable<Student>[] UnassignedStudents(IList<Student> students, IList<Team>[] schedule) public static IEnumerable<Student>[] UnassignedStudents(IList<Student> students, IList<Team>[] schedule)
=> schedule.Select(ts => UnassignedStudents(students, ts)).ToArray(); => schedule.Select(ts => UnassignedStudents(students, ts)).ToArray();
public TeamSchedulerSolution ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy) public Team[] ScheduleStrategy(UnassignedScheduleStrategy scheduleStrategy)
{ {
var ss = scheduleStrategy switch var assignments = scheduleStrategy switch
{ {
UnassignedScheduleStrategy.BiggestGroup => ScheduleStrategy(GetAvailableTeams_BiggestGroup), UnassignedScheduleStrategy.BiggestGroup => ScheduleStrategy(GetAvailableTeams_BiggestGroup),
UnassignedScheduleStrategy.IndividualEvents => ScheduleStrategy(GetAvailableTeams_Individual), UnassignedScheduleStrategy.IndividualEvents => ScheduleStrategy(GetAvailableTeams_Individual),
@@ -32,14 +32,15 @@ public class UnassignedStudentScheduler
_ => throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null) _ => throw new ArgumentOutOfRangeException(nameof(scheduleStrategy), scheduleStrategy, null)
}; };
return new TeamSchedulerSolution(ss, "Success?"); return assignments;
} }
public Team[][] ScheduleStrategy(Func<IEnumerable<Team>, IEnumerable<Student>, IEnumerable<Team>> availableTeamSelector) public Team[] ScheduleStrategy(Func<IEnumerable<Team>, IEnumerable<Student>, IEnumerable<Team>> availableTeamSelector)
{ {
// Find stuff for unassigned students in each timeslot // Find stuff for unassigned students in each timeslot
var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList(); var scheduledTeams = _timeSlots.SelectMany(list => list).Distinct().ToList();
foreach (var slot in _timeSlots) var additions = new List<Team>();
foreach (var slot in _timeSlots)
{ {
var unassigned = UnassignedStudents(_students, slot).ToList(); var unassigned = UnassignedStudents(_students, slot).ToList();
while (unassigned.Count > 0) while (unassigned.Count > 0)
@@ -54,20 +55,21 @@ public class UnassignedStudentScheduler
slot.Add(teamToAdd); slot.Add(teamToAdd);
scheduledTeams.Add(teamToAdd); scheduledTeams.Add(teamToAdd);
additions.Add(teamToAdd);
foreach (var student in teamToAdd.Students) foreach (var student in teamToAdd.Students)
unassigned.Remove(student); unassigned.Remove(student);
} }
} }
return _timeSlots.Select(e => e.ToArray()).ToArray(); return additions.ToArray();
} }
// find teams where several unassigned students can work together // find teams where several unassigned students can work together
private IEnumerable<Team> GetAvailableTeams_BiggestGroup( private IEnumerable<Team> GetAvailableTeams_BiggestGroup(
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) => IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
_teams _teams
.Where(t => scheduledTeams.All(st => st.Identifier != t.Identifier)) .Where(t => scheduledTeams.All(st => st.Id != t.Id))
.Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Select(t => t.CloneWithOmittedStudents(assignedStudents))
.Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual .Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual
//.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name)) //.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name))
@@ -78,7 +80,7 @@ public class UnassignedStudentScheduler
private IEnumerable<Team> GetAvailableTeams_Individual( private IEnumerable<Team> GetAvailableTeams_Individual(
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) => IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
_teams _teams
.Where(t => scheduledTeams.All(st => st.Identifier != t.Identifier)) .Where(t => scheduledTeams.All(st => st.Id != t.Id))
.Where(t => t.Event.EventFormat == EventFormat.Individual || t.Students.Count == 1) .Where(t => t.Event.EventFormat == EventFormat.Individual || t.Students.Count == 1)
.Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Select(t => t.CloneWithOmittedStudents(assignedStudents))
.Where(t => t.Students.Count > 0); .Where(t => t.Students.Count > 0);
@@ -87,7 +89,7 @@ public class UnassignedStudentScheduler
private IEnumerable<Team> GetAvailableTeams_AnyNotMeetingAlready( private IEnumerable<Team> GetAvailableTeams_AnyNotMeetingAlready(
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) => IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
_teams _teams
.Where(t => scheduledTeams.All(st => st.Identifier != t.Identifier)) .Where(t => scheduledTeams.All(st => st.Id != t.Id))
.Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Select(t => t.CloneWithOmittedStudents(assignedStudents))
.Where(t => t.Students.Count > 0); .Where(t => t.Students.Count > 0);
@@ -102,7 +104,7 @@ public class UnassignedStudentScheduler
private IEnumerable<Team> GetAvailableTeams_LevelOfEffort( private IEnumerable<Team> GetAvailableTeams_LevelOfEffort(
IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) => IEnumerable<Team> scheduledTeams, IEnumerable<Student> assignedStudents) =>
_teams _teams
.Where(t => scheduledTeams.All(st => st.Identifier != t.Identifier)) .Where(t => scheduledTeams.All(st => st.Id != t.Id))
.Select(t => t.CloneWithOmittedStudents(assignedStudents)) .Select(t => t.CloneWithOmittedStudents(assignedStudents))
.Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual .Where(t => t.Students.Count > 1) //|| t.Event.EventFormat is EventFormat.Individual
//.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name)) //.OrderBy(t => scheduledTeams.Count(st => st.Name == t.Name))
+4 -3
View File
@@ -5,6 +5,7 @@ public class Team
{ {
public int Id { get; set; } public int Id { get; set; }
[Required]
public EventDefinition Event { get; set; } public EventDefinition Event { get; set; }
public List<Student> Students { get; set; } = []; public List<Student> Students { get; set; } = [];
@@ -47,11 +48,11 @@ public class Team
{ {
var studentsToOmitList = studentsToOmit.ToList(); var studentsToOmitList = studentsToOmit.ToList();
var omittedStudents = Students.Where(studentsToOmitList.Contains).ToList(); var omittedStudents = Students.Where(studentsToOmitList.Contains).ToList();
if (!omittedStudents.Any()) if (omittedStudents.Count == 0)
return new Team{Captain = Captain, Event = Event, Students = Students.ToList(), Identifier = Identifier}; return new Team{Captain = Captain, Event = Event, Students = Students.ToList(), Identifier = Identifier, Id = Id};
var remainingStudents = Students.Where(s => !studentsToOmitList.Contains(s)).ToList(); var remainingStudents = Students.Where(s => !studentsToOmitList.Contains(s)).ToList();
return new PartialTeam { Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents}; return new PartialTeam { Event = Event, Students = remainingStudents, OmittedStudents = omittedStudents, Identifier = Identifier, Id = Id};
} }
public Team Clone() => CloneWithOmittedStudents([]); public Team Clone() => CloneWithOmittedStudents([]);
+5 -5
View File
@@ -25,11 +25,11 @@ namespace Data
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
} }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ //{
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ChapterOrganizer.db"); // var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ChapterOrganizer.db");
optionsBuilder.UseSqlite($"Data Source={dbPath}"); // optionsBuilder.UseSqlite($"Data Source={dbPath}");
} //}
} }
public class EventDefinitionConfiguration : IEntityTypeConfiguration<EventDefinition> public class EventDefinitionConfiguration : IEntityTypeConfiguration<EventDefinition>
Binary file not shown.
+2 -2
View File
@@ -18,7 +18,7 @@ public class EventAssignmentTests
var eventAssignment = new EventAssignment(events, students, new AssignmentParameters()); var eventAssignment = new EventAssignment(events, students, new AssignmentParameters());
var solution = eventAssignment.Solve().Result; var solution = eventAssignment.Solve().Result;
var teamWriter = new TeamWriter(solution.Teams, @"c:\temp\teams.csv"); //var teamWriter = new TeamWriter(solution.Teams, @"c:\temp\teams.csv");
teamWriter.Write(); //teamWriter.Write();
} }
} }
+2 -2
View File
@@ -48,8 +48,8 @@ public class TeamSchedulerTest
solution = teamScheduler.Solve(); solution = teamScheduler.Solve();
} }
solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.BiggestGroup); //solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.BiggestGroup);
solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.IndividualEvents); //solution = new UnassignedStudentScheduler(allTeams, solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.IndividualEvents);
var i = 1; var i = 1;
foreach (var slot in solution.TimeSlots) foreach (var slot in solution.TimeSlots)
+4 -4
View File
@@ -18,7 +18,7 @@ public static class TestEntityHandler
public static FileInfo GetEventOccurrenceFileInfo() public static FileInfo GetEventOccurrenceFileInfo()
{ {
return FileUtility.GetContentFile(ContentDirectory, "2025 TSA Nationals Competition Events Times.txt"); return FileUtility.GetContentFile(ContentDirectory, "2025 TSA Nationals Competition Event Times.txt");
} }
public static Student[] GetStudents(IList<EventDefinition> events) public static Student[] GetStudents(IList<EventDefinition> events)
@@ -35,7 +35,7 @@ public static class TestEntityHandler
public static Team[] GetTeams(IList<EventDefinition> events, IList<Student> students) public static Team[] GetTeams(IList<EventDefinition> events, IList<Student> students)
{ {
//var studentEventRankingsCsv = "Student Event Rankings.csv"; //var studentEventRankingsCsv = "Student Event Rankings.csv";
var studentEventRankingsCsv = "2023-24 RMS TSA student & event - TimeSlots.csv"; var studentEventRankingsCsv = "2023-24 RMS TSA student & event - Teams.csv";
var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv); var fileInfo = FileUtility.GetContentFile(ContentDirectory, studentEventRankingsCsv);
var eventRankingsParser = new TeamParser(fileInfo); var eventRankingsParser = new TeamParser(fileInfo);
@@ -50,14 +50,14 @@ public static class TestEntityHandler
public static AssignmentRequirement[] GetEventRequirements(IList<EventDefinition> events, IList<Student> students) public static AssignmentRequirement[] GetEventRequirements(IList<EventDefinition> events, IList<Student> students)
{ {
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024-25 RMS TSA student & eventDefinition - assumptions.csv"); var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024-25 RMS TSA student & event - assumptions.csv");
var assumptionParser = new AssignmentRequirementParser(fileInfo); var assumptionParser = new AssignmentRequirementParser(fileInfo);
return assumptionParser.Parse(events, students); return assumptionParser.Parse(events, students);
} }
public static StudentEventRanking[] GetStudentEventRankings(Student[] students, EventDefinition[] events) public static StudentEventRanking[] GetStudentEventRankings(Student[] students, EventDefinition[] events)
{ {
var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024-25 RMS TSA - Student Event Rankings.csv"); var fileInfo = FileUtility.GetContentFile(ContentDirectory, "2024-25 RMS TSA student & event - Student Event Rankings.csv");
var rankingParser = new StudentEventRankingParser(fileInfo); var rankingParser = new StudentEventRankingParser(fileInfo);
return rankingParser.Parse(students, events); return rankingParser.Parse(students, events);
+16 -7
View File
@@ -2,13 +2,22 @@
namespace WebApp namespace WebApp
{ {
public class ChapterSettings public class StateContainer
{ {
public required string Name { get; set; } private int[]? _scheduledTeams;
public required string ShortName { get; set; }
public required string RegionalId { get; set; } public int[] UserId
public required string StateId { get; set; } {
public required string NationalId { get; set; } get => _scheduledTeams ?? [];
public required string CompetitionYear { get; set; } set
{
_scheduledTeams = value;
NotifyStateChanged();
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
} }
} }
+22
View File
@@ -0,0 +1,22 @@
using System.Threading.Tasks;
using Microsoft.JSInterop;
public sealed class ClipboardService
{
private readonly IJSRuntime _jsRuntime;
public ClipboardService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public ValueTask<string> ReadTextAsync()
{
return _jsRuntime.InvokeAsync<string>("navigator.clipboard.readText");
}
public ValueTask WriteTextAsync(string text)
{
return _jsRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
<MudNavLink Href="/teams/printout" Icon="@Icons.Material.Filled.Print">Print out</MudNavLink> <MudNavLink Href="/teams/printout" Icon="@Icons.Material.Filled.Print">Print out</MudNavLink>
<MudNavLink Href="/teams/handout" Icon="@Icons.Material.Filled.Print">Handout</MudNavLink> <MudNavLink Href="/teams/handout" Icon="@Icons.Material.Filled.Print">Handout</MudNavLink>
</MudNavGroup> </MudNavGroup>
<MudNavLink Href="/meeting-schedule/scheduler" Icon="@AppIcons.Scheduler">Schedule</MudNavLink> <MudNavLink Href="/meeting-schedule/" Icon="@AppIcons.Scheduler">Schedule</MudNavLink>
<MudNavGroup Title="Team Building" Icon="@Icons.Material.Filled.GroupAdd" Expanded="true"> <MudNavGroup Title="Team Building" Icon="@Icons.Material.Filled.GroupAdd" Expanded="true">
<MudNavLink Href="/students/event-ranking" Icon="@AppIcons.EventRank">Event Ranking</MudNavLink> <MudNavLink Href="/students/event-ranking" Icon="@AppIcons.EventRank">Event Ranking</MudNavLink>
<MudNavLink Href="/teams/assignment" Icon="@AppIcons.TeamAssignment">Team Assignment</MudNavLink> <MudNavLink Href="/teams/assignment" Icon="@AppIcons.TeamAssignment">Team Assignment</MudNavLink>
+24 -2
View File
@@ -1,5 +1,5 @@
@using WebApp.Models @using WebApp.Models
@*
<MudTooltip Text="Level of Effort">@AppIcons.LevelOfEffortIcon(EventDefinition.LevelOfEffort)</MudTooltip> <MudTooltip Text="Level of Effort">@AppIcons.LevelOfEffortIcon(EventDefinition.LevelOfEffort)</MudTooltip>
@if(EventDefinition.EventFormat == EventFormat.Individual) { @if(EventDefinition.EventFormat == EventFormat.Individual) {
@@ -7,7 +7,7 @@
} }
@if (EventDefinition.OnSiteActivity) @if (EventDefinition.OnSiteActivity)
{ {
<MudTooltip Text="On-site Activity">@AppIcons.OnSiteActivity</MudTooltip> <MudTooltip Text="On-site Activity"> @AppIcons.OnSiteActivity</MudTooltip>
} }
@if (EventDefinition.RegionalEvent) @if (EventDefinition.RegionalEvent)
@@ -18,6 +18,28 @@
@if (EventDefinition.Presubmission) @if (EventDefinition.Presubmission)
{ {
<MudTooltip Text="Presubmission Event">@AppIcons.PresubmissionEvent</MudTooltip> <MudTooltip Text="Presubmission Event">@AppIcons.PresubmissionEvent</MudTooltip>
} *@
@AppIcons.LevelOfEffortIcon(EventDefinition.LevelOfEffort)
@if (EventDefinition.EventFormat == EventFormat.Individual)
{
@AppIcons.IndividualEvent
}
@if (EventDefinition.OnSiteActivity)
{
@AppIcons.OnSiteActivity
}
@if (EventDefinition.RegionalEvent)
{
@AppIcons.RegionalEvent
}
@if (EventDefinition.Presubmission)
{
@AppIcons.PresubmissionEvent
} }
@@ -0,0 +1,327 @@
@using System.Text
@using Core.Calculation
@using Microsoft.EntityFrameworkCore
@page "/meeting-schedule"
@inject IConfiguration Configuration
@inject AppDbContext Context
@inject ClipboardService ClipboardService
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</MudText>
<MudPaper Class="pa-4 mt-5">
@* <MudText>Include: @string.Join(", ", _scheduledTeams) </MudText> *@
<MudGrid>
<MudItem xs="6" lg="9">
<MudText Typo="Typo.h4">Time Slots</MudText>
<MudNumericField @bind-Value="_parameters.TimeSlots"
Label="Time Slots" Min="1" Max="4"></MudNumericField>
<MudButton Class="ma-3" OnClick="Solve" Variant="Variant.Filled" Color="Color.Primary" Disabled="@_isSolving">Solve</MudButton>
<MudIconButton OnClick="CopyToClipboard" Icon="@Icons.Material.Filled.ContentCopy">
</MudIconButton>
<MudTable T="Team[]" ServerData="SolveSchedule" @ref="_solutionData">
<HeaderContent>
</HeaderContent>
<RowTemplate>
<MudTd>
@{
var overlaps
= TeamSchedulerSolution.GetStudentTeamOverlaps(context).ToArray();
}
@foreach (var team in context.OrderBy(e => e.ToString()))
{
var removed = !_scheduledTeams.Contains(team);
<MudItem>
<MudLink Typo="Typo.body1"
Class="d-flex align-center"
Color="Color.Default"
OnClick="@(() => ToggleRequiredTeam(team))">
<MudIcon Icon="@Icons.Material.Filled.Clear"
Size="Size.Small"
Class="@(removed ? "" : "d-none")"></MudIcon>
@team -
@foreach (var student in team.Students)
{
var overlap = overlaps.Any(o => o.Item1.Equals(student));
var color = overlap ? Color.Warning : Color.Default;
<MudText
Typo="Typo.body2"
Class="d-inline-block ml-3"
Color="color">
&nbsp;@student.FirstName@(overlap ? "*" : "")
</MudText>
}
</MudLink>
</MudItem>
}
</MudTd>
<MudTd>
@{
var unscheduled = TeamSchedulerSolution.GetStudentsNotInTimSlot(context, _students);
}
@if (unscheduled.Any())
{
<MudItem>Unscheduled</MudItem>
foreach (var student in unscheduled)
{
<MudItem Class="">
<MudText Typo="Typo.body1" HtmlTag="i">@student.FirstName </MudText>&nbsp;
@{
var pa = _possibleAdditions.ToArray();
}
@foreach (var unassignedTeam in _solution.StudentUnassignedTeams(student))
{
var color = pa.Contains(unassignedTeam) ? Color.Error : Color.Default;
var added = _scheduledTeams.Contains(unassignedTeam);
<MudLink Typo="Typo.body2"
Class="d-flex align-center ml-3"
Color="@color"
OnClick="@(() => ToggleRequiredTeam(unassignedTeam))">
<MudIcon Icon="@Icons.Material.Filled.Check"
Size="Size.Small"
Class="@(added ? "" : "d-none")"></MudIcon>
@unassignedTeam
</MudLink>
}
</MudItem>
}
}
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem xs="6" lg="3">
<MudStack>
<MudButton OnClick="() => AddHighLevelOfEffort()">Add High Effort</MudButton>
<MudButton OnClick="() => AddRegionals()">Add Regionals</MudButton>
<MudButton OnClick="() => RemoveIndividual()">Remove Individual</MudButton>
<MudButton OnClick="() => RemoveLowLevelOfEffort()">Remove Low Effort</MudButton>
<MudButton OnClick="() => Invert()">Invert</MudButton>
<MudItem>@string.Join(", ", (_possibleAdditions ?? []).Select(e => e.ToString()))</MudItem>
<MudToggleGroup T="Team"
SelectionMode="SelectionMode.MultiSelection"
@bind-Values="_scheduledTeams"
Vertical="true"
CheckMark>
@foreach (var team in _teams.OrderBy(e => e.Event.Name))
{
<MudToggleItem Value="@team" Style="font-size: .75rem;">
<MudTooltip Text="@string.Join(", ", team.Students.Select(s => s.FirstName))">
<div class="d-flex align-center justify-space-between flex-wrap">
<MudText Class="ellipsis">@team.ToString()</MudText>
<EventAttributes EventDefinition="@team.Event"></EventAttributes>
</div>
</MudTooltip>
</MudToggleItem>
}
</MudToggleGroup>
</MudStack>
</MudItem>
</MudGrid>
</MudPaper>
@code {
private Team[]? _teams;
private Student[]? _students;
MudTable<Team[]> _solutionData;
private TeamSchedulerSolution _solution;
private TeamSchedulerOptions _parameters;
bool _isSolving;
private IEnumerable<Team> _scheduledTeams = [];
private IEnumerable<Team> _possibleAdditions = [];
private async Task AddRegionals()
{
_scheduledTeams
= _teams.Where(e => e.Event.RegionalEvent).Concat(_scheduledTeams).Distinct();
}
private async Task AddHighLevelOfEffort()
{
_scheduledTeams
= _teams.Where(e => e.Event.LevelOfEffort >= 3).Concat(_scheduledTeams).Distinct();
}
private async Task RemoveIndividual()
{
_scheduledTeams
= _scheduledTeams.Where(t => t.Event.EventFormat != EventFormat.Individual);
}
private async Task RemoveLowLevelOfEffort()
{
_scheduledTeams
= _scheduledTeams.Where(t => t.Event.LevelOfEffort > 1);
}
private async Task Invert()
{
var rt = _scheduledTeams.ToArray();
_scheduledTeams
= _teams.Where(t => !rt.Contains(t));
}
private void ToggleRequiredTeam(Team unassignedTeam)
{
if (_scheduledTeams.Contains(unassignedTeam))
_scheduledTeams = _scheduledTeams.Where(t => t != unassignedTeam);
else
{
_scheduledTeams = _scheduledTeams.Concat(new[] { unassignedTeam });
}
}
protected override async Task OnInitializedAsync()
{
_parameters =
new TeamSchedulerOptions(
timeSlots: 2,
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:
[
]
);
_teams
= await Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.OrderBy(e => e.Event.Name)
.ThenBy(e => e.Identifier)
.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<TableData<Team[]>> SolveSchedule(TableState arg1, CancellationToken arg2)
{
_isSolving = true;
var teamScheduler = new TeamScheduler(_scheduledTeams, _parameters.TimeSlots);
// teamScheduler
// .ScheduleSeparate(
// _teams.First(e => e.Event.Name.Contains("Data Science")),
// _teams.First(e => e.Event.Name.Contains("Microcontroller Design"))
// );
_solution = teamScheduler.Solve();
var loe = new UnassignedStudentScheduler(_teams, _solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.LevelOfEffort);
var biggest = new UnassignedStudentScheduler(_teams, _solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.BiggestGroup);
var individual = new UnassignedStudentScheduler(_teams, _solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.IndividualEvents);
var anyNotMeetingAlready = new UnassignedStudentScheduler(_teams, _solution.TimeSlots).ScheduleStrategy(UnassignedScheduleStrategy.AnyNotMeetingAlready);
_possibleAdditions = loe;
if (!_possibleAdditions.Any())
_possibleAdditions = biggest;
if (!_possibleAdditions.Any())
_possibleAdditions = anyNotMeetingAlready;
if (!_possibleAdditions.Any())
_possibleAdditions = individual;
await InvokeAsync(StateHasChanged); // let the UI know that the solution has been found
_isSolving = false;
return new TableData<Team[]> { Items = _solution.TimeSlots};
}
private void Solve()
{
_solutionData.ReloadServerData();
}
async Task CopyToClipboard()
{
var sb = new StringBuilder();
foreach (var timeslot in _solution.TimeSlots)
{
var overlaps
= TeamSchedulerSolution.GetStudentTeamOverlaps(timeslot).Select(e => e.Item1).ToArray();
foreach (var scheduledTeam in timeslot.OrderBy(e => e.ToString()))
{
var t = scheduledTeam.ToString();
var s =
string.Join(", ",
scheduledTeam.Students
.OrderBy(e => e == scheduledTeam.Captain)
.ThenBy(e => e.FirstName)
.Select(e => e.FirstName + (overlaps.Contains(e) ? "*": "")));
if (scheduledTeam.Event.EventFormat is EventFormat.Individual)
sb.Append(t);
else
sb.Append($"{t} - {s}");
sb.Append(Environment.NewLine);
}
var unscheduled = TeamSchedulerSolution.GetStudentsNotInTimSlot(timeslot, _students);
if (unscheduled.Any())
{
sb.Append("--Unscheduled");
sb.Append(Environment.NewLine);
foreach (var student in unscheduled)
{
var s = student.FirstName;
var unassignedTeams = _solution.StudentUnassignedTeams(student);
var t = string.Join(", ", unassignedTeams.Select(e => e.ToString()));
sb.Append($"{s} - {t}");
sb.Append(Environment.NewLine);
}
}
sb.Append(Environment.NewLine);
}
try
{
await ClipboardService.WriteTextAsync(sb.ToString());
}
catch
{
Console.WriteLine("Cannot write text to clipboard");
}
}
}
@@ -1,287 +0,0 @@
@using Core.Calculation
@using Microsoft.EntityFrameworkCore
@page "/meeting-schedule/scheduler"
@inject IConfiguration Configuration
@inject AppDbContext Context
<PageTitle>@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</PageTitle>
<MudText Typo="Typo.h3">@Configuration["ChapterSettings:Shortname"] TSA Schedule @Configuration["ChapterSettings:CompetitionYear"]</MudText>
<MudPaper Class="pa-4 mt-5">
<MudGrid>
<MudItem Style="width:160px;">
<MudNumericField @bind-Value="_parameters.TimeSlots"
Label="Time Slots" Min="1" Max="4"></MudNumericField>
</MudItem>
@foreach (var evt in _options)
{
<MudItem Style="width:200px">
<MudButtonGroup>
<MudTooltip Text="Require">
<MudToggleIconButton @bind-Toggled="@evt.Require"
Icon="@Icons.Material.Filled.CheckBoxOutlineBlank"
ToggledIcon="@Icons.Material.Filled.CheckBox"
ToggledColor="@Color.Success"
Disabled="@evt.Omit"
Size="Size.Small"
title="@(evt.Require ? "Required" : "Optional")" />
</MudTooltip>
<MudTooltip Text="Extend">
<MudToggleIconButton @bind-Toggled="@evt.Extend"
Icon="@Icons.Material.Filled.PlusOne"
ToggledIcon="@Icons.Material.Filled.PlusOne"
ToggledColor="@Color.Success"
Disabled="@evt.Omit"
Size="Size.Small"
title="@(evt.Extend ? "Extend" : "Single")" />
</MudTooltip>
<MudTooltip Text="Extend">
<MudToggleIconButton @bind-Toggled="@evt.Omit"
Icon="@Icons.Material.Filled.EventBusy"
ToggledIcon="@Icons.Material.Filled.EventBusy"
ToggledColor="@Color.Error"
Size="Size.Small"
title="@(evt.Omit ? "Omit" : "Optional")" />
</MudTooltip>
</MudButtonGroup>
<MudTooltip Text="@string.Join(", ", evt.Team.Students.Select(e => e.FirstName))">
<MudText>@evt.Team.ToString()</MudText>
</MudTooltip>
</MudItem>
}
@* <MudItem>
<MudTooltip Text="Require at least one On-Site Event">
<MudSwitch @bind-Value="_parameters" Color="Color.Info"
Label="On-Site" />
</MudTooltip>
</MudItem>
<MudItem>
<MudTooltip Text="Require at least one Regional Event">
<MudSwitch @bind-Value="_parameters.RequireRegional" Color="Color.Info"
Label="Regional" />
</MudTooltip>
</MudItem>
<MudItem>
<MudStack Style="width:100px;">
<MudTooltip Text="Student Event Count Assignment Range">
<MudInputLabel>Event Count</MudInputLabel>
</MudTooltip>
<MudNumericField @bind-Value="_parameters.EventsLowerBound"
Label="At Least" Min="2" Max="4"></MudNumericField>
<MudNumericField @bind-Value="_parameters.EventsUpperBound"
Label="Up to" Min="3" Max="5"></MudNumericField>
</MudStack>
</MudItem>
<MudItem>
<MudStack Style="width:100px;">
<MudTooltip Text="Student Level of Effort Range">
<MudInputLabel>LOE</MudInputLabel>
</MudTooltip>
<MudNumericField @bind-Value="_parameters.EffortLowerBound"
Label="At Least" Min="4" Max="7"></MudNumericField>
<MudNumericField @bind-Value="_parameters.EffortUpperBound"
Label="Up to" Min="7" Max="12"></MudNumericField>
</MudStack>
</MudItem>
<MudItem>
<MudInputLabel>Assignment Requirements</MudInputLabel>
<MudTable T="AssignmentRequirement" ServerData="ReloadAssignmentRequirements" @ref="_assignmentRequirementData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveRequireEvent(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">
@item.Student.FirstName
@item.EventDefinition.ShortName
@if (item.Requirement == Requirement.Include)
{
<MudIcon Class="ml-3" Icon="@Icons.Material.Filled.ThumbUp" Size="Size.Small"></MudIcon>
}
@if (item.Requirement == Requirement.Exclude)
{
<MudIcon Class="ml-3" Icon="@Icons.Material.Filled.ThumbDownAlt" Size="Size.Small"></MudIcon>
}
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem>
<MudInputLabel>Two Team Events</MudInputLabel>
<MudTable T="EventDefinition" ServerData="ReloadEventTwoTeam" @ref="_eventTwoTeamData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveTwoTeam(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.ShortName</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
<MudItem>
<MudInputLabel>Omitted Events</MudInputLabel>
<MudTable T="EventDefinition" ServerData="ReloadOmittedEvents" @ref="_eventOmittedData">
<RowTemplate Context="item">
<MudTd Class="align-center">
<MudIconButton Icon="@Icons.Material.Filled.RemoveCircle" Size="Size.Small"
OnClick="() => RemoveOmitted(item)"></MudIconButton>
</MudTd>
<MudTd Class="align-center">@item.ShortName</MudTd>
</RowTemplate>
</MudTable>
</MudItem> *@
</MudGrid>
<MudButton Class="ma-3" OnClick="Solve" Variant="Variant.Filled" Color="Color.Primary" Disabled="@_isSolving">Solve</MudButton>
</MudPaper>
<MudItem xs="12" lg="4">
<MudText Typo="Typo.h4">Time Slots</MudText>
<MudTable T="Team[]" ServerData="SolveSchedule" @ref="_solutionData">
<HeaderContent>
</HeaderContent>
<RowTemplate>
<MudTd>
@{
var ol = TeamSchedulerSolution.GetStudentTeamOverlaps(context);}
@foreach (var t in context)
{
<MudItem>
@t.ToString() -
@string.Join(", ", t.Students.Select(s => s.FirstName + " " + (ol.Any(o => o.Item1.Equals(s)) ? "*" : "" )) )
</MudItem>
}
@* @foreach (var overlap in ol)
{
<MudItem>
@string.Join(", ", overlap.Item1)
</MudItem>
} *@
@{ var notInTimeSLot = TeamSchedulerSolution.GetStudentsNotInTimSlot(context, _students); }
@if (notInTimeSLot.Any()) {
<MudItem>
<i>
Not scheduled: @string.Join(", ", notInTimeSLot.Select(s => s.FirstName))
</i>
</MudItem>
}
</MudTd>
</RowTemplate>
</MudTable>
</MudItem>
@code {
private Team[]? _teams;
private Student[]? _students;
MudTable<Team[]> _solutionData;
private TeamSchedulerSolution _solution;
private TeamSchedulerOptions _parameters;
bool _isSolving = false;
public class TeamOptions
{
public Team Team { get; set; }
public bool Require { get; set; } = false;
public bool Omit { get; set; } = false;
public bool Extend { get; set; } = false;
}
private TeamOptions[]? _options;
protected override async Task OnInitializedAsync()
{
_parameters =
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:
[
]
);
_teams
= await Context.Teams
.Include(e => e.Event)
.Include(e => e.Students)
.OrderBy(e => e.Event.Name)
.ThenBy(e => e.Identifier)
.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();
_options = _teams
.Select(e => new TeamOptions { Team = e })
.ToArray();
}
private async Task<TableData<Team[]>> SolveSchedule(TableState arg1, CancellationToken arg2)
{
//_isSolving = true;
var mustIncludeTeams = _teams;
mustIncludeTeams = mustIncludeTeams.Where(t => t.Event.EventFormat != EventFormat.Individual).ToArray();
mustIncludeTeams = mustIncludeTeams.Where(t => !t.ToString().Contains("#1")).ToArray();
//mustIncludeTeams = mustIncludeTeams.Where(t => !t.ToString().Contains("Photo")).ToArray();
var teamScheduler = new TeamScheduler(mustIncludeTeams, 3);
// 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<Team[]> { Items = _solution.TimeSlots};
}
private void Solve()
{
_solutionData.ReloadServerData();
}
}
@@ -39,20 +39,20 @@ else
@if (!context.RankedEvents.Any(re => re.OnSiteActivity)) @if (!context.RankedEvents.Any(re => re.OnSiteActivity))
{ {
<MudTooltip Text="No On-Site Activity"> <MudTooltip Text="No On-Site Activity">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.LocalActivity"></MudIcon> <MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.OnSiteActivity</MudText>
</MudTooltip> </MudTooltip>
} }
@if (!context.RankedEvents.Any(re => re.RegionalEvent)) @if (!context.RankedEvents.Any(re => re.RegionalEvent))
{ {
<MudTooltip Text="No Regional Event"> <MudTooltip Text="No Regional Event">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.PinDrop"></MudIcon> <MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.RegionalEvent</MudText>
</MudTooltip> </MudTooltip>
} }
@if (context.RankedEvents.All(re => re.EventFormat != EventFormat.Individual)) @if (context.RankedEvents.All(re => re.EventFormat != EventFormat.Individual))
{ {
<MudTooltip Text="No Individual Event"> <MudTooltip Text="No Individual Event">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.Person"></MudIcon> <MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.IndividualEvent</MudText>
</MudTooltip> </MudTooltip>
} }
</MudTd> </MudTd>
@@ -64,9 +64,7 @@ else
@if (st != null) @if (st != null)
{ {
<span>@st.EventDefinition.ShortName&nbsp; <span>@st.EventDefinition.ShortName&nbsp;
@if(st.EventDefinition.EventFormat == EventFormat.Individual) { <span>ⓘ</span>} <EventAttributes EventDefinition="@st.EventDefinition"></EventAttributes>
@if(st.EventDefinition.RegionalEvent) {<span>ⓡ</span>}
@if(st.EventDefinition.OnSiteActivity) {<span>ⓐ</span>}
</span> </span>
} }
</MudTd> </MudTd>
@@ -4,6 +4,7 @@
@using WebApp.Models @using WebApp.Models
@using EventAssignment = Core.Calculation.EventAssignment @using EventAssignment = Core.Calculation.EventAssignment
@inject AppDbContext Context @inject AppDbContext Context
@inject IDialogService DialogService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<PageTitle>Event Assignment - TSA Chapter Organizer</PageTitle> <PageTitle>Event Assignment - TSA Chapter Organizer</PageTitle>
@@ -135,13 +136,13 @@
<MudTd>@if (!context.HasOnSiteActivity) <MudTd>@if (!context.HasOnSiteActivity)
{ {
<MudTooltip Text="No On-Site Activity"> <MudTooltip Text="No On-Site Activity">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.LocalActivity"></MudIcon> <MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.OnSiteActivity</MudText>
</MudTooltip> </MudTooltip>
} }
@if (!context.HasRegionalEvent) @if (!context.HasRegionalEvent)
{ {
<MudTooltip Text="No Regional Event"> <MudTooltip Text="No Regional Event">
<MudIcon Color="Color.Warning" Icon="@Icons.Material.Filled.PinDrop"></MudIcon> <MudText Style="font-weight:bolder" Color="Color.Warning">@AppIcons.RegionalEvent</MudText>
</MudTooltip> </MudTooltip>
} }
</MudTd> </MudTd>
@@ -238,7 +239,7 @@
</MudTable> </MudTable>
</MudItem> </MudItem>
<MudItem xs="12" lg="4"> <MudItem xs="12" lg="4">
<MudText Typo="Typo.h4">TimeSlots</MudText> <MudText Typo="Typo.h4">Teams</MudText>
<MudTable T="Team" ServerData="SolveAssignments" @ref="_teamData"> <MudTable T="Team" ServerData="SolveAssignments" @ref="_teamData">
<ColGroup> <ColGroup>
<col style="width: 200px;" /> <col style="width: 200px;" />
@@ -254,7 +255,7 @@
} }
<MudTd> <MudTd>
<b>@context.Event.Name</b>&nbsp; <b>@context.Event.Name</b>&nbsp;
<span>@AppIcons.EventEffort(context.Event) @AppIcons.EventAttributes(context.Event)</span> <EventAttributes EventDefinition="context.Event"></EventAttributes>
</MudTd> </MudTd>
<MudTd Style="white-space:nowrap"> <MudTd Style="white-space:nowrap">
@@ -444,11 +445,19 @@
public async Task SaveTeams() public async Task SaveTeams()
{ {
var teams = await Context.Teams.ExecuteDeleteAsync(); var result = await DialogService
.ShowMessageBox("Save Teams",
(MarkupString)$"Are you sure want to save these teams? Current teams will be erased. This cannot be undone.",
yesText: "Yes",
noText: "Cancel");
await Context.Teams.AddRangeAsync(_teams); if (result == true)
await Context.SaveChangesAsync(); {
await Context.Teams.ExecuteDeleteAsync();
await Context.Teams.AddRangeAsync(_teams);
await Context.SaveChangesAsync();
NavigationManager.NavigateTo("/teams"); NavigationManager.NavigateTo("/teams");
}
} }
} }
+9 -1
View File
@@ -31,7 +31,15 @@ else
<MudSelectItem T="Student" Value="@student">@student.Name</MudSelectItem> <MudSelectItem T="Student" Value="@student">@student.Name</MudSelectItem>
} }
</MudSelect> </MudSelect>
<MudTextField T="Student" Label="Captain" @bind-Value="Team.Captain" For="@(() => Team.Captain)" Required="false" Clearable="true"></MudTextField> <MudStack>
<MudText>Captain</MudText>
<MudToggleGroup T="Student" SelectionMode="SelectionMode.ToggleSelection" @bind-Value="Team.Captain" CheckMark>
@foreach (var student in _selectedStudents.OrderBy(e => e.FirstName))
{
<MudToggleItem Value="@(student)" Text="@student.Name" />
}
</MudToggleGroup>
</MudStack>
<MudTextField T="string?" Label="Identifier" @bind-Value="Team.Identifier" For="@(() => Team.Identifier)" Required="false" Clearable="true"></MudTextField> <MudTextField T="string?" Label="Identifier" @bind-Value="Team.Identifier" For="@(() => Team.Identifier)" Required="false" Clearable="true"></MudTextField>
</MudPaper> </MudPaper>
</MudItem> </MudItem>
@@ -1,6 +1,5 @@
@page "/teams" @page "/teams"
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using WebApp.Models
@inject AppDbContext Context @inject AppDbContext Context
@inject IDialogService DialogService @inject IDialogService DialogService
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@@ -31,11 +30,6 @@
<TeamStudents Team="@context.Item"></TeamStudents> <TeamStudents Team="@context.Item"></TeamStudents>
</CellTemplate> </CellTemplate>
</TemplateColumn> </TemplateColumn>
@* <TemplateColumn Title="Grade (TSA Year)" SortBy="e => e.Grade" Sortable="true">
<CellTemplate>
@context.Item.Grade (@context.Item.TsaYear)
</CellTemplate>
</TemplateColumn> *@
<TemplateColumn> <TemplateColumn>
<CellTemplate> <CellTemplate>
<CrudActions <CrudActions
@@ -47,7 +41,7 @@
</TemplateColumn> </TemplateColumn>
</Columns> </Columns>
<PagerContent> <PagerContent>
<MudDataGridPager T="Student"></MudDataGridPager> <MudDataGridPager T="Team"></MudDataGridPager>
</PagerContent> </PagerContent>
</MudDataGrid> </MudDataGrid>
@@ -33,14 +33,14 @@ else
@context.ToString() @context.ToString()
</MudTd> </MudTd>
<MudTd> <MudTd>
@AppIcons.EventEffort(context.Event) <EventAttributes EventDefinition="context.Event"></EventAttributes>
@AppIcons.EventAttributes(context.Event)
</MudTd> </MudTd>
@{ @{
var students var students
= context.Students = context.Students
.OrderBy(s => s.EventRankings.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue) .OrderByDescending(s => s == context.Captain)
.ThenBy(s => s.EventRankings.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue)
.ThenByDescending(e => e.Grade) .ThenByDescending(e => e.Grade)
.ThenBy(e => e.FirstName) .ThenBy(e => e.FirstName)
.ToArray(); .ToArray();
@@ -54,8 +54,8 @@ else
var rank = student.EventRankings var rank = student.EventRankings
.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue; .Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue;
<MudTd Class="@($"event-rank-{rank}")"> <MudTd Class="@(EventRankClass(rank))">
@student.Name @student.Name @if(context?.Captain == student) {<span> (Cpt)</span>}
</MudTd> </MudTd>
} }
else else
@@ -117,10 +117,9 @@ else
{ {
var rank = context.EventRankings var rank = context.EventRankings
.Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue; .Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue;
<MudTh Class="@($"event-rank-{rank}")"> <MudTh Class="@(EventRankClass(rank))">
@team.ToString() @team.ToString()
@AppIcons.EventEffort(team.Event) <EventAttributes EventDefinition="team.Event"></EventAttributes>
@AppIcons.EventAttributes(team.Event)
@if (rank == int.MaxValue) @if (rank == int.MaxValue)
{ {
@@ -181,7 +180,7 @@ else
<RowTemplate> <RowTemplate>
@{ var rank = context.EventRankings @{ var rank = context.EventRankings
.Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue; } .Find(e => e.EventDefinition == team.Event)?.Rank ?? int.MaxValue; }
<MudTd Class="@($"event-rank-{rank}")"> <MudTd Class="@(EventRankClass(rank))">
@team.ToString() @team.ToString()
@AppIcons.EventEffort(team.Event) @AppIcons.EventEffort(team.Event)
@AppIcons.EventAttributes(team.Event) @AppIcons.EventAttributes(team.Event)
@@ -205,6 +204,14 @@ else
private Team[]? _teams; private Team[]? _teams;
private int _maxTeamSize; private int _maxTeamSize;
private Student[]? _students; private Student[]? _students;
private bool _rankColorEnabled;
private string EventRankClass(int rank)
{
if (!_rankColorEnabled)
return "";
return "event-rank-" + rank;
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -16,7 +16,7 @@
@student.FirstName @student.FirstName
@if (captain) @if (captain)
{ {
<span> *</span> <span> (Cpt)</span>
} }
</MudPaper> </MudPaper>
} }
@@ -0,0 +1,18 @@
<MudSelect
T="Team"
MultiSelection="true"
@bind-SelectedValues="@SelectedTeams"
ToStringFunc="e => e.ToString()"
Label="@Label">
@foreach (var evt in Teams.OrderBy(e => e.ToString()))
{
<MudSelectItem T="Team" Value="@evt">@evt.ToString()</MudSelectItem>
}
</MudSelect>
@code {
[Parameter] public Team[]? Teams { get; set; }
[Parameter] public required IEnumerable<Team> SelectedTeams { get; set; }
[Parameter] public string Label { get; set; } = "Teams";
}
+1 -1
View File
@@ -29,4 +29,4 @@ RUN dotnet publish "./WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:
FROM base AS final FROM base AS final
WORKDIR /app WORKDIR /app
COPY --from=publish /app/publish . COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApp.dll"] ENTRYPOINT ["dotnet", "tsa-chapter-organizer.dll"]
+6
View File
@@ -1,6 +1,7 @@
using Data; using Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using MudBlazor.Services; using MudBlazor.Services;
using WebApp;
using WebApp.Components; using WebApp.Components;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -19,6 +20,11 @@ builder.Services.AddQuickGridEntityFrameworkAdapter();
builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddScoped<ClipboardService>();
builder.Services.AddScoped<StateContainer>(); // Server- side
builder.Services.AddSingleton<StateContainer>();//Client-side
var app = builder.Build(); var app = builder.Build();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
@@ -14,3 +14,4 @@
<_TargetId>DockerCustomContainerRegistry</_TargetId> <_TargetId>DockerCustomContainerRegistry</_TargetId>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
<!-- https://stackoverflow.com/questions/59713258/how-to-customize-the-docker-image-name-in-vs2019 -->
+10
View File
@@ -55,6 +55,16 @@
}, },
"publishAllPorts": true, "publishAllPorts": true,
"useSSL": true "useSSL": true
},
"Scheduler": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "meeting-schedule",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7235;http://localhost:5013"
} }
}, },
"$schema": "http://json.schemastore.org/launchsettings.json", "$schema": "http://json.schemastore.org/launchsettings.json",
+2 -1
View File
@@ -6,8 +6,9 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>73972335-ec46-4ad6-a959-8ebe0b06147d</UserSecretsId> <UserSecretsId>73972335-ec46-4ad6-a959-8ebe0b06147d</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<PackageId>tsa-chapter-organizer</PackageId> <PackageId>$(AssemblyName)</PackageId>
<Title>TSA Chapter Organizer</Title> <Title>TSA Chapter Organizer</Title>
<AssemblyName>tsa-chapter-organizer</AssemblyName>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+1 -1
View File
@@ -7,7 +7,7 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ConnectionStrings": { "ConnectionStrings": {
"SQLiteDefault": "Data Source=ChapterOrganizer.db" "SQLiteDefault": "Data Source=data.db"
}, },
"Name" : "Test" , "Name" : "Test" ,
"ChapterSettings": { "ChapterSettings": {