Add student name formatting utilities and refactor team student name handling
This commit introduces two new utility classes: StudentNameFormatter and TeamStudentNameFormatter, which provide methods for formatting student names with options for indicating absence and overlaps. The Team class has been updated to remove the StudentsFirstNames property, and various components across the WebApp have been refactored to utilize the new formatting utilities. This enhances the maintainability and readability of the code while improving the presentation of student names in the UI.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
@using Heron.MudCalendar
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using WebApp.Authentication
|
||||
@using Core.Utility
|
||||
@inject IEventOccurrenceService EventOccurrenceService
|
||||
@inject ILogger<Index> Logger
|
||||
@inject IDialogService DialogService
|
||||
@@ -110,8 +111,15 @@
|
||||
}
|
||||
|
||||
// Get student first names for this event definition
|
||||
var studentFirstNames = occ.EventDefinition != null
|
||||
? StudentFirstNames(occ.EventDefinition, teamsByEventId)
|
||||
var studentFirstNames = occ.EventDefinition != null && teamsByEventId.TryGetValue(occ.EventDefinition.Id, out var teams)
|
||||
? TeamStudentNameFormatter.FormatStudentListForEvent(
|
||||
occ.EventDefinition,
|
||||
teams,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Star,
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.Alphabetical
|
||||
})
|
||||
: [];
|
||||
|
||||
var calendarItem = new CalendarEventItem(occ, studentFirstNames);
|
||||
@@ -143,34 +151,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> StudentFirstNames(EventDefinition ed, Dictionary<int, List<Team>> teamsByEventId)
|
||||
{
|
||||
List<string> studentFirstNames = [];
|
||||
if (ed?.Id == null || !teamsByEventId.TryGetValue(ed.Id, out var teams)) return studentFirstNames;
|
||||
|
||||
// Get all unique student first names from all teams for this event
|
||||
// Include captain indicator (*) for team events
|
||||
var allStudents = teams
|
||||
.SelectMany(t => t.Students)
|
||||
.DistinctBy(s => s.Id) // Ensure uniqueness by student ID
|
||||
.Select(s =>
|
||||
{
|
||||
var isCaptain = teams.Any(t => t.Captain?.Id == s.Id);
|
||||
var name = s.FirstName;
|
||||
// Add star for captain in team events (EventFormat == Team)
|
||||
if (isCaptain && ed.EventFormat == Core.Entities.EventFormat.Team)
|
||||
{
|
||||
name += "*";
|
||||
}
|
||||
return name;
|
||||
})
|
||||
.OrderBy(name => name)
|
||||
.ToList();
|
||||
|
||||
studentFirstNames = allStudents;
|
||||
|
||||
return studentFirstNames;
|
||||
}
|
||||
|
||||
private DateTime GetNextDateWithEvents()
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using Core.Calculation
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Utility
|
||||
@inject IConfiguration Configuration
|
||||
@inject AppDbContext Context
|
||||
@inject ClipboardService ClipboardService
|
||||
@@ -380,21 +381,44 @@
|
||||
|
||||
private string FormatStudentList(Team team, TeamScheduleTimeSlot timeslot)
|
||||
{
|
||||
return string.Join(", ",
|
||||
team.Students
|
||||
.OrderBy(e => e == team.Captain)
|
||||
.ThenBy(e => e.FirstName)
|
||||
.Select(e => FormatStudentName(e, timeslot)));
|
||||
return TeamStudentNameFormatter.FormatStudentList(
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.CaptainFirst,
|
||||
MarkOverlaps = true,
|
||||
HasOverlaps = timeslot.StudentHasOverlaps,
|
||||
MarkAbsent = true,
|
||||
AbsentStudents = _absentStudents.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private string FormatStudentName(Student student, TeamScheduleTimeSlot timeslot)
|
||||
{
|
||||
var name = student.FirstName;
|
||||
if (timeslot.StudentHasOverlaps(student))
|
||||
name += "*";
|
||||
if (_absentStudents.Contains(student))
|
||||
name += " (absent)";
|
||||
return name;
|
||||
// Find the team this student belongs to for formatting context
|
||||
var team = _teams.FirstOrDefault(t => t.Students.Contains(student));
|
||||
if (team == null)
|
||||
{
|
||||
// No team context, use StudentNameFormatter directly
|
||||
return StudentNameFormatter.FormatStudentName(
|
||||
student,
|
||||
new StudentNameFormatter.FormatOptions
|
||||
{
|
||||
HasOverlap = timeslot.StudentHasOverlaps(student),
|
||||
IsAbsent = _absentStudents.Contains(student)
|
||||
});
|
||||
}
|
||||
|
||||
return TeamStudentNameFormatter.FormatStudentName(
|
||||
student,
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
MarkOverlaps = true,
|
||||
HasOverlaps = timeslot.StudentHasOverlaps,
|
||||
MarkAbsent = true,
|
||||
AbsentStudents = _absentStudents.ToList()
|
||||
});
|
||||
}
|
||||
|
||||
private void AppendUnscheduledStudents(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
||||
@@ -407,9 +431,12 @@
|
||||
|
||||
foreach (var student in timeslot.UnscheduledStudents)
|
||||
{
|
||||
var studentName = student.FirstName;
|
||||
if (_absentStudents.Contains(student))
|
||||
studentName += " (absent)";
|
||||
var studentName = StudentNameFormatter.FormatStudentName(
|
||||
student,
|
||||
new StudentNameFormatter.FormatOptions
|
||||
{
|
||||
IsAbsent = _absentStudents.Contains(student)
|
||||
});
|
||||
|
||||
var unassignedTeams = _solution.StudentUnassignedTeams(student);
|
||||
var teamsList = string.Join(", ", unassignedTeams.Select(e => e.ToString()));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@using Core.Calculation
|
||||
@using WebApp.Models
|
||||
@using Core.Utility
|
||||
|
||||
<MudStack>
|
||||
<MudText Typo="Typo.h6">@TimeSlotName</MudText>
|
||||
@@ -8,31 +9,54 @@
|
||||
var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
var removed = !scheduledTeamIds.Contains(team.Id);
|
||||
|
||||
<MudLink Typo="Typo.body1"
|
||||
Class="d-flex align-center"
|
||||
Color="Color.Default"
|
||||
OnClick="@(() => OnToggleTeam.InvokeAsync(team))">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Clear"
|
||||
Size="Size.Small"
|
||||
Class="@(removed ? "" : "d-none")">
|
||||
</MudIcon>
|
||||
@team -
|
||||
@foreach (var student in team.Students)
|
||||
{
|
||||
var overlap = StudentHasOverlaps(student);
|
||||
var isAbsent = AbsentStudents.Contains(student);
|
||||
var color = overlap ? Color.Warning : Color.Default;
|
||||
var suffix = GetStudentSuffix(overlap, isAbsent);
|
||||
|
||||
if (student != team.Students.First())
|
||||
{
|
||||
<MudText>, </MudText>
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="d-flex align-center">
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Clear"
|
||||
Size="Size.Small"
|
||||
Class="@(removed ? "" : "d-none")"
|
||||
OnClick="@(() => OnToggleTeam.InvokeAsync(team))"
|
||||
Style="cursor: pointer;">
|
||||
</MudIcon>
|
||||
@{
|
||||
var teamMembers = TeamStudentNameFormatter.FormatStudentList(
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.None
|
||||
});
|
||||
}
|
||||
<MudText Typo="Typo.body2" Color="@color">
|
||||
@student.FirstName@suffix
|
||||
</MudText>
|
||||
}
|
||||
</MudLink>
|
||||
<MudTooltip Text="@teamMembers">
|
||||
<div @onclick="@(() => OnToggleTeam.InvokeAsync(team))" style="cursor: pointer; display: inline-block;">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="Color.Default">
|
||||
@team
|
||||
</MudChip>
|
||||
</div>
|
||||
</MudTooltip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
|
||||
@foreach (var student in team.Students)
|
||||
{
|
||||
var overlap = StudentHasOverlaps(student);
|
||||
var chipColor = overlap ? Color.Warning : Color.Default;
|
||||
var formattedName = TeamStudentNameFormatter.FormatStudentName(
|
||||
student,
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
MarkOverlaps = true,
|
||||
HasOverlaps = StudentHasOverlaps,
|
||||
MarkAbsent = true,
|
||||
AbsentStudents = AbsentStudents.ToList()
|
||||
});
|
||||
|
||||
<MudChip T="string" Size="Size.Small" Color="@chipColor" Variant="Variant.Outlined">
|
||||
@formattedName
|
||||
</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@@ -54,11 +78,4 @@
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Team> OnToggleTeam { get; set; }
|
||||
|
||||
private string GetStudentSuffix(bool overlap, bool isAbsent)
|
||||
{
|
||||
var suffix = overlap ? "*" : "";
|
||||
suffix += isAbsent ? " (absent)" : "";
|
||||
return suffix;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using Core.Calculation
|
||||
@using Core.Utility
|
||||
|
||||
@if (UnscheduledStudents.Any())
|
||||
{
|
||||
@@ -6,32 +7,51 @@
|
||||
<MudStack>
|
||||
@foreach (var student in UnscheduledStudents)
|
||||
{
|
||||
var isAbsent = AbsentStudents.Contains(student);
|
||||
<MudItem>
|
||||
<MudText Typo="Typo.body1" HtmlTag="i">
|
||||
@student.FirstName@(isAbsent ? " (absent)" : "")
|
||||
</MudText>
|
||||
@foreach (var unassignedTeam in UnassignedTeams(student))
|
||||
{
|
||||
var isPossibleAddition = PossibleAdditions.Contains(unassignedTeam, new TeamIdComparer());
|
||||
var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
var isScheduled = scheduledTeamIds.Contains(unassignedTeam.Id);
|
||||
var color = isPossibleAddition ? Color.Success : Color.Default;
|
||||
|
||||
if (unassignedTeam != UnassignedTeams(student).First())
|
||||
{
|
||||
<span>, </span>
|
||||
}
|
||||
<MudLink Typo="Typo.body2"
|
||||
Color="@color"
|
||||
OnClick="@(() => OnToggleTeam.InvokeAsync(unassignedTeam))">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check"
|
||||
Size="Size.Small"
|
||||
Class="@(isScheduled ? "" : "d-none")">
|
||||
</MudIcon>
|
||||
@unassignedTeam
|
||||
</MudLink>
|
||||
@{
|
||||
var formattedName = StudentNameFormatter.FormatStudentName(
|
||||
student,
|
||||
new StudentNameFormatter.FormatOptions
|
||||
{
|
||||
IsAbsent = AbsentStudents.Contains(student)
|
||||
});
|
||||
}
|
||||
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
||||
<MudStack Row="true" Spacing="1" AlignItems="AlignItems.Center">
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Class="font-style-italic">
|
||||
@formattedName
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap" AlignItems="AlignItems.Center">
|
||||
@foreach (var unassignedTeam in UnassignedTeams(student))
|
||||
{
|
||||
var isPossibleAddition = PossibleAdditions.Contains(unassignedTeam, new TeamIdComparer());
|
||||
var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
var isScheduled = scheduledTeamIds.Contains(unassignedTeam.Id);
|
||||
var chipColor = isPossibleAddition ? Color.Success : Color.Default;
|
||||
var teamMembers = TeamStudentNameFormatter.FormatStudentList(
|
||||
unassignedTeam,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.None
|
||||
});
|
||||
|
||||
<MudTooltip Text="@teamMembers">
|
||||
<div @onclick="@(() => OnToggleTeam.InvokeAsync(unassignedTeam))" style="cursor: pointer; display: inline-block;">
|
||||
<MudChip T="string"
|
||||
Size="Size.Small"
|
||||
Color="@chipColor">
|
||||
@if (isScheduled)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Size="Size.Small" Style="margin-right: 4px;" />
|
||||
}
|
||||
@unassignedTeam
|
||||
</MudChip>
|
||||
</div>
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
@using WebApp.Models
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Validation
|
||||
@using Core.Utility
|
||||
@inject AppDbContext Context
|
||||
@inject WebApp.LocalStorageService LocalStorage
|
||||
@inject ValidationService ValidationService
|
||||
@@ -83,7 +84,12 @@
|
||||
@foreach (var team in teamsToDisplay)
|
||||
{
|
||||
var isCaptain = team.Captain != null && team.Captain.Equals(context.Item.Student);
|
||||
var teamMembers = string.Join(", ", team.Students.Select(s => s.FirstName));
|
||||
var teamMembers = TeamStudentNameFormatter.FormatStudentList(
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.None
|
||||
});
|
||||
|
||||
<MudTooltip Text="@teamMembers">
|
||||
<MudChip Size="Size.Small"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@using WebApp.Models
|
||||
@using Core.Utility
|
||||
|
||||
@if (Title != null)
|
||||
{
|
||||
@@ -14,14 +15,20 @@
|
||||
@foreach (var team in Teams.OrderByEventFormatFirst().ThenBy(e => e.Event.Name))
|
||||
{
|
||||
<MudToggleItem Value="@team" Style="font-size: .75rem;">
|
||||
<MudTooltip Text="@team.StudentsFirstNames">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1" Wrap="Wrap.Wrap">
|
||||
<MudText Class="ellipsis">@team.ToString()</MudText>
|
||||
<MudTooltip Text="@TeamStudentNameFormatter.FormatStudentList(
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Captain,
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.None
|
||||
})">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; gap: 4px; width: 100%;">
|
||||
<span class="ellipsis" style="flex: 1; min-width: 0;">@team.ToString()</span>
|
||||
@if (ShowEventAttributes)
|
||||
{
|
||||
<EventAttributes EventDefinition="@team.Event"></EventAttributes>
|
||||
}
|
||||
</MudStack>
|
||||
</div>
|
||||
</MudTooltip>
|
||||
</MudToggleItem>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Models
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Utility
|
||||
@inject IConfiguration Configuration
|
||||
@inject AppDbContext Context
|
||||
|
||||
@@ -35,7 +36,12 @@ else
|
||||
@if (team.Event.EventFormat == EventFormat.Team)
|
||||
{
|
||||
<span style="font-weight: normal; font-size: 0.9em;">
|
||||
(Team: @string.Join(", ", team.Students.OrderByDescending(e => e.Grade + e.TsaYear).Select(e => e.FirstName)))
|
||||
(Team: @TeamStudentNameFormatter.FormatStudentList(
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.GradeDescending
|
||||
}))
|
||||
</span>
|
||||
}
|
||||
</MudText>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Models
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Utility
|
||||
@inject IConfiguration Configuration
|
||||
@inject AppDbContext Context
|
||||
|
||||
@@ -57,7 +58,12 @@ else
|
||||
{
|
||||
<MudItem xs="12">
|
||||
<MudText Class="d-flex py-1" Typo="Typo.h6">
|
||||
Team Members: @string.Join(", ", team.Students.OrderByDescending(e => e.Grade + e.TsaYear).Select(e => e.FirstName))
|
||||
Team Members: @TeamStudentNameFormatter.FormatStudentList(
|
||||
team,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.GradeDescending
|
||||
})
|
||||
</MudText>
|
||||
</MudItem>
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@
|
||||
= Context.Teams
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Event)
|
||||
.Include(e => e.Captain)
|
||||
.Include(e => e.Students)
|
||||
.ThenInclude(e => e.EventRankings);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Models
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Utility
|
||||
@inject IConfiguration Configuration
|
||||
@inject AppDbContext Context
|
||||
|
||||
@@ -56,7 +57,13 @@ else
|
||||
.Find(e => e.EventDefinition == context.Event)?.Rank ?? int.MaxValue;
|
||||
|
||||
<MudTd Class="@(EventRankClass(rank))">
|
||||
@student.Name @if(context?.Captain == student) {<span> (Cpt)</span>}
|
||||
@TeamStudentNameFormatter.FormatStudentName(
|
||||
student,
|
||||
context,
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Captain
|
||||
})
|
||||
</MudTd>
|
||||
}
|
||||
else
|
||||
@@ -191,7 +198,12 @@ else
|
||||
<span>❔</span>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd>@string.Join(", ", team.Students.Where(e => e != context).Select(e => e.FirstName))</MudTd>
|
||||
<MudTd>@TeamStudentNameFormatter.FormatStudentList(
|
||||
new Team { Students = team.Students.Where(e => e != context).ToList(), Event = team.Event },
|
||||
new TeamStudentNameFormatter.FormatOptions
|
||||
{
|
||||
Ordering = TeamStudentNameFormatter.OrderingStyle.None
|
||||
})</MudTd>
|
||||
<MudTd></MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
|
||||
Reference in New Issue
Block a user