Keep legal first and last names for formal lists; show DisplayFirstName on teams, calendars, and import matching so two Josiahs can be told apart. Co-authored-by: Cursor <cursoragent@cursor.com>
262 lines
9.7 KiB
C#
262 lines
9.7 KiB
C#
using Core.Entities;
|
|
|
|
namespace Core.Utility;
|
|
|
|
/// <summary>
|
|
/// Utility class for formatting student names for teams with various formatting options.
|
|
/// </summary>
|
|
public static class TeamStudentNameFormatter
|
|
{
|
|
/// <summary>
|
|
/// Options for formatting student names.
|
|
/// </summary>
|
|
public record FormatOptions
|
|
{
|
|
/// <summary>
|
|
/// Style for indicating the team captain. Default is None.
|
|
/// </summary>
|
|
public CaptainIndicatorStyle CaptainIndicator { get; init; } = CaptainIndicatorStyle.None;
|
|
|
|
/// <summary>
|
|
/// How to order the students. Default is None (preserve original order).
|
|
/// </summary>
|
|
public OrderingStyle Ordering { get; init; } = OrderingStyle.None;
|
|
|
|
/// <summary>
|
|
/// Whether to add "*" suffix for students with schedule overlaps. Default is false.
|
|
/// </summary>
|
|
public bool MarkOverlaps { get; init; } = false;
|
|
|
|
/// <summary>
|
|
/// Whether to add "(absent)" suffix for absent students. Default is false.
|
|
/// </summary>
|
|
public bool MarkAbsent { get; init; } = false;
|
|
|
|
/// <summary>
|
|
/// Function to determine if a student has overlaps. Required if MarkOverlaps is true.
|
|
/// </summary>
|
|
public Func<Student, bool>? HasOverlaps { get; init; }
|
|
|
|
/// <summary>
|
|
/// Collection of absent students. Required if MarkAbsent is true.
|
|
/// </summary>
|
|
public ICollection<Student>? AbsentStudents { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether to only show captain indicator for team events (EventFormat.Team). Default is true.
|
|
/// </summary>
|
|
public bool OnlyTeamEventsForCaptain { get; init; } = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Style for indicating the team captain.
|
|
/// </summary>
|
|
public enum CaptainIndicatorStyle
|
|
{
|
|
/// <summary>
|
|
/// No captain indicator.
|
|
/// </summary>
|
|
None,
|
|
|
|
/// <summary>
|
|
/// Use asterisk (*) for captain.
|
|
/// </summary>
|
|
Star,
|
|
|
|
/// <summary>
|
|
/// Use "(Cpt)" for captain.
|
|
/// </summary>
|
|
Captain
|
|
}
|
|
|
|
/// <summary>
|
|
/// Style for ordering students.
|
|
/// </summary>
|
|
public enum OrderingStyle
|
|
{
|
|
/// <summary>
|
|
/// Preserve original order.
|
|
/// </summary>
|
|
None,
|
|
|
|
/// <summary>
|
|
/// Captain first, then alphabetical by first name.
|
|
/// </summary>
|
|
CaptainFirst,
|
|
|
|
/// <summary>
|
|
/// Alphabetical by first name.
|
|
/// </summary>
|
|
Alphabetical,
|
|
|
|
/// <summary>
|
|
/// By grade + TSA year descending (highest first).
|
|
/// </summary>
|
|
GradeDescending
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats a single student name with the specified options.
|
|
/// </summary>
|
|
/// <param name="student">The student to format.</param>
|
|
/// <param name="team">The team the student belongs to.</param>
|
|
/// <param name="options">Formatting options.</param>
|
|
/// <returns>Formatted student name.</returns>
|
|
public static string FormatStudentName(Student student, Team team, FormatOptions options)
|
|
{
|
|
if (student == null)
|
|
return string.Empty;
|
|
|
|
var name = student.DisplayFirstName;
|
|
|
|
// Add captain indicator (before overlap/absent markers)
|
|
if (options.CaptainIndicator != CaptainIndicatorStyle.None && team != null && team.Captain != null && team.Captain.Equals(student))
|
|
{
|
|
var shouldShow = !options.OnlyTeamEventsForCaptain || (team.Event != null && team.Event.EventFormat == EventFormat.Team);
|
|
if (shouldShow)
|
|
{
|
|
name += options.CaptainIndicator switch
|
|
{
|
|
CaptainIndicatorStyle.Star => "*",
|
|
CaptainIndicatorStyle.Captain => "(Cpt)",
|
|
_ => string.Empty
|
|
};
|
|
}
|
|
}
|
|
|
|
// Convert collections/functions to booleans for StudentNameFormatter
|
|
var hasOverlap = options.MarkOverlaps && options.HasOverlaps != null && options.HasOverlaps(student);
|
|
var isAbsent = options.MarkAbsent && options.AbsentStudents != null && options.AbsentStudents.Contains(student);
|
|
|
|
// Use StudentNameFormatter for overlap/absent markers (appended after captain indicator)
|
|
var studentNameOptions = new StudentNameFormatter.FormatOptions
|
|
{
|
|
HasOverlap = hasOverlap,
|
|
IsAbsent = isAbsent
|
|
};
|
|
|
|
// Get the suffix from StudentNameFormatter (overlap/absent markers)
|
|
var baseFormatted = StudentNameFormatter.FormatStudentName(student, studentNameOptions);
|
|
var suffix = baseFormatted.Substring(student.DisplayFirstName.Length);
|
|
|
|
return name + suffix;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats all students for a team as a comma-separated string.
|
|
/// </summary>
|
|
/// <param name="team">The team to format students for.</param>
|
|
/// <param name="options">Formatting options.</param>
|
|
/// <returns>Comma-separated string of formatted student names.</returns>
|
|
public static string FormatStudentList(Team team, FormatOptions options)
|
|
{
|
|
if (team?.Students == null || !team.Students.Any())
|
|
return string.Empty;
|
|
|
|
var students = ApplyOrdering(team.Students, team, options.Ordering);
|
|
|
|
return string.Join(", ", students.Select(s => FormatStudentName(s, team, options)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats all students for a team as a list of strings.
|
|
/// </summary>
|
|
/// <param name="team">The team to format students for.</param>
|
|
/// <param name="options">Formatting options.</param>
|
|
/// <returns>List of formatted student names.</returns>
|
|
public static List<string> FormatStudentListAsList(Team team, FormatOptions options)
|
|
{
|
|
if (team?.Students == null || !team.Students.Any())
|
|
return [];
|
|
|
|
var students = ApplyOrdering(team.Students, team, options.Ordering);
|
|
|
|
return students.Select(s => FormatStudentName(s, team, options)).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Formats all unique students across multiple teams for an event definition.
|
|
/// Returns a list of unique student names (by student ID).
|
|
/// </summary>
|
|
/// <param name="eventDefinition">The event definition.</param>
|
|
/// <param name="teams">The teams for this event.</param>
|
|
/// <param name="options">Formatting options.</param>
|
|
/// <returns>List of unique formatted student names.</returns>
|
|
public static List<string> FormatStudentListForEvent(EventDefinition eventDefinition, IEnumerable<Team> teams, FormatOptions options)
|
|
{
|
|
if (eventDefinition == null || teams == null)
|
|
return [];
|
|
|
|
var teamsList = teams.ToList();
|
|
if (!teamsList.Any())
|
|
return [];
|
|
|
|
// Get all unique students from all teams for this event
|
|
var allStudents = teamsList
|
|
.SelectMany(t => t.Students)
|
|
.DistinctBy(s => s.Id)
|
|
.ToList();
|
|
|
|
if (!allStudents.Any())
|
|
return [];
|
|
|
|
// Determine if each student is a captain in any team
|
|
var studentsWithCaptainInfo = allStudents.Select(s =>
|
|
{
|
|
var isCaptain = teamsList.Any(t => t.Captain != null && t.Captain.Equals(s));
|
|
// Find a team this student belongs to for formatting context
|
|
var studentTeam = teamsList.FirstOrDefault(t => t.Students.Contains(s));
|
|
return new { Student = s, IsCaptain = isCaptain, Team = studentTeam };
|
|
}).ToList();
|
|
|
|
// Apply ordering
|
|
var orderedStudents = options.Ordering switch
|
|
{
|
|
OrderingStyle.CaptainFirst => studentsWithCaptainInfo
|
|
.OrderBy(x => !x.IsCaptain)
|
|
.ThenBy(x => x.Student.DisplayFirstName)
|
|
.Select(x => x.Student),
|
|
OrderingStyle.Alphabetical => studentsWithCaptainInfo
|
|
.OrderBy(x => x.Student.DisplayFirstName)
|
|
.Select(x => x.Student),
|
|
OrderingStyle.GradeDescending => studentsWithCaptainInfo
|
|
.OrderByDescending(x => x.Student.Grade + x.Student.TsaYear)
|
|
.Select(x => x.Student),
|
|
_ => studentsWithCaptainInfo.Select(x => x.Student)
|
|
};
|
|
|
|
// Format names - for event-level formatting, we need to handle captain indicator specially
|
|
// since a student might be captain in one team but not another
|
|
return orderedStudents.Select(s =>
|
|
{
|
|
var studentInfo = studentsWithCaptainInfo.First(x => x.Student.Equals(s));
|
|
// Use the student's team if available, otherwise create a minimal team for formatting context
|
|
var team = studentInfo.Team ?? new Team { Students = [s], Event = eventDefinition };
|
|
|
|
// Create options that handle captain indicator for event-level
|
|
var eventOptions = options with
|
|
{
|
|
CaptainIndicator = studentInfo.IsCaptain &&
|
|
(!options.OnlyTeamEventsForCaptain || eventDefinition.EventFormat == EventFormat.Team)
|
|
? options.CaptainIndicator
|
|
: CaptainIndicatorStyle.None
|
|
};
|
|
|
|
return FormatStudentName(s, team, eventOptions);
|
|
}).ToList();
|
|
}
|
|
|
|
private static IEnumerable<Student> ApplyOrdering(IEnumerable<Student> students, Team team, OrderingStyle ordering)
|
|
{
|
|
return ordering switch
|
|
{
|
|
OrderingStyle.CaptainFirst => students
|
|
.OrderBy(s => team.Captain == null || !team.Captain.Equals(s))
|
|
.ThenBy(s => s.DisplayFirstName),
|
|
OrderingStyle.Alphabetical => students.OrderBy(s => s.DisplayFirstName),
|
|
OrderingStyle.GradeDescending => students.OrderByDescending(s => s.Grade + s.TsaYear),
|
|
_ => students
|
|
};
|
|
}
|
|
}
|