feat: merge event rankings and attribute badges into the page printer
Interview notes can print each student's ranked events and a shared attribute legend from the same catalog as the ranking index, without leftover table markup or collapsed answer space. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -313,6 +313,8 @@ else
|
||||
yield return ("Layout", PrintFieldCatalog.Layout);
|
||||
yield return ("Chapter", PrintFieldCatalog.Chapter);
|
||||
yield return (_entityType.ToString(), PrintFieldCatalog.EntityTokens(_entityType));
|
||||
if (_entityType == PrintEntityType.Student)
|
||||
yield return ("Event ranks", PrintFieldCatalog.StudentRanks);
|
||||
if (_entityType == PrintEntityType.Student && _importedTokenNames.Count > 0)
|
||||
yield return ("Additional fields", _importedTokenNames);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
@using WebApp.Models
|
||||
@using Core.Printing
|
||||
@using WebApp.Models
|
||||
<MudPaper>
|
||||
<h3>Legend</h3>
|
||||
|
||||
<MudContainer>
|
||||
<ul>
|
||||
<li>@AppIcons.LevelOfEffortIcon(1) - Level of Effort </li>
|
||||
<li>@AppIcons.IndividualEvent - Individual Event </li>
|
||||
<li>@AppIcons.RegionalEvent - Regional </li>
|
||||
<li>@AppIcons.OnSiteActivity - On-site Activity</li>
|
||||
<li>@AppIcons.PresubmissionEvent - Pre-submission</li>
|
||||
<li>@AppIcons.PresentationEvent - Interview Or Presentation</li>
|
||||
@foreach (var mark in EventAttributeMarks.LegendItems)
|
||||
{
|
||||
<li>@mark.Symbol - @mark.Label</li>
|
||||
}
|
||||
</ul>
|
||||
</MudContainer>
|
||||
</MudPaper>
|
||||
</MudPaper>
|
||||
|
||||
+24
-91
@@ -1,4 +1,5 @@
|
||||
using Core.Entities;
|
||||
using Core.Printing;
|
||||
using MudBlazor;
|
||||
|
||||
namespace WebApp.Models
|
||||
@@ -14,50 +15,26 @@ namespace WebApp.Models
|
||||
public static string Captain = Icons.Material.Filled.Star;
|
||||
public static string Registration = Icons.Material.Filled.AppRegistration;
|
||||
public static string EventCalendar = Icons.Material.Filled.Event;
|
||||
public static string LevelOfEffortIcon(int? loe)
|
||||
{
|
||||
|
||||
return loe switch
|
||||
public static string LevelOfEffortIcon(int? loe) =>
|
||||
loe switch
|
||||
{
|
||||
1 => "○",
|
||||
2 => "◐",
|
||||
3 => "⬤",
|
||||
1 => EventAttributeMarks.LevelOfEffort1,
|
||||
2 => EventAttributeMarks.LevelOfEffort2,
|
||||
3 => EventAttributeMarks.LevelOfEffort3,
|
||||
_ => Icons.Material.Filled.QuestionMark
|
||||
};
|
||||
}
|
||||
|
||||
/*https://unicodeplus.com/search*/
|
||||
public static string OnSiteActivity = "ⓐ";
|
||||
public static string RegionalEvent = "ⓡ";
|
||||
public static string IndividualEvent = "ⓘ";
|
||||
public static string PresubmissionEvent = "ⓟ";
|
||||
public static string PresentationEvent = "";
|
||||
public static string OnSiteActivity => EventAttributeMarks.OnSite;
|
||||
public static string RegionalEvent => EventAttributeMarks.Regional;
|
||||
public static string IndividualEvent => EventAttributeMarks.Individual;
|
||||
public static string PresubmissionEvent => EventAttributeMarks.Presubmission;
|
||||
public static string PresentationEvent => "";
|
||||
|
||||
// Tooltip mapping for icon unicode characters
|
||||
public static Dictionary<string, string> IconTooltips => new()
|
||||
{
|
||||
{ OnSiteActivity, "On-Site Activity" },
|
||||
{ RegionalEvent, "Regional Event" },
|
||||
{ IndividualEvent, "Individual Event" },
|
||||
{ PresubmissionEvent, "Presubmission" },
|
||||
{ PresentationEvent, "Presentation/Interview" },
|
||||
{ "○", "Level of Effort: 1" },
|
||||
{ "◐", "Level of Effort: 2" },
|
||||
{ "●", "Level of Effort: 3" }
|
||||
};
|
||||
public static IReadOnlyDictionary<string, string> IconTooltips { get; } =
|
||||
EventAttributeMarks.LegendItems.ToDictionary(m => m.Symbol, m => m.Label, StringComparer.Ordinal);
|
||||
|
||||
// Color mapping for icon unicode characters
|
||||
public static Dictionary<string, string> IconColors => new()
|
||||
{
|
||||
{ OnSiteActivity, "#ff9800" }, // Orange
|
||||
{ RegionalEvent, "#2196f3" }, // Blue
|
||||
{ IndividualEvent, "#9c27b0" }, // Purple
|
||||
{ PresubmissionEvent, "#4caf50" }, // Green
|
||||
{ PresentationEvent, "#f44336" }, // Red
|
||||
{ "○", "#757575" }, // Gray
|
||||
{ "◐", "#616161" }, // Darker Gray
|
||||
{ "●", "#424242" } // Even Darker Gray
|
||||
};
|
||||
public static IReadOnlyDictionary<string, string> IconColors { get; } =
|
||||
EventAttributeMarks.LegendItems.ToDictionary(m => m.Symbol, m => m.Color, StringComparer.Ordinal);
|
||||
|
||||
public static string EventEffort(EventDefinition eventDefinition)
|
||||
{
|
||||
@@ -96,63 +73,19 @@ namespace WebApp.Models
|
||||
};
|
||||
}
|
||||
|
||||
public static string RankedEventColor(int rank)
|
||||
{
|
||||
return rank switch
|
||||
{
|
||||
1 => "#dd7e6b",
|
||||
2 => "#ea9999",
|
||||
3 => "#f9cb9c",
|
||||
4 => "#ffe599",
|
||||
5 => "#fff2cc",
|
||||
6 => "#fffaea",
|
||||
7 => "#fffefa",
|
||||
8 => "#fffefc",
|
||||
9 => "#fffffd",
|
||||
10 => "#fffffe",
|
||||
_ => "#ddd"
|
||||
};
|
||||
}
|
||||
public static string RankedEventColor(int rank) => EventRankLegend.ColorHex(rank);
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
public static string GetOrdinal(int num) => EventRankLegend.Ordinal(num);
|
||||
|
||||
public static string GetOrdinalSuperscript(int number)
|
||||
{
|
||||
var suffix = number switch
|
||||
{
|
||||
11 or 12 or 13 => "th",
|
||||
_ => (number % 10) switch
|
||||
{
|
||||
1 => "st",
|
||||
2 => "nd",
|
||||
3 => "rd",
|
||||
_ => "th"
|
||||
}
|
||||
};
|
||||
return $"{number}<sup>{suffix}</sup>";
|
||||
var ordinal = EventRankLegend.Ordinal(number);
|
||||
var suffixAt = 0;
|
||||
while (suffixAt < ordinal.Length && (char.IsDigit(ordinal[suffixAt]) || ordinal[suffixAt] == '-'))
|
||||
suffixAt++;
|
||||
return suffixAt is 0 || suffixAt == ordinal.Length
|
||||
? ordinal
|
||||
: $"{ordinal[..suffixAt]}<sup>{ordinal[suffixAt..]}</sup>";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -58,6 +58,8 @@ public class NotePrintService : INotePrintService
|
||||
query = query.Where(s => s.OfficerRole == null);
|
||||
|
||||
var students = await query
|
||||
.Include(s => s.EventRankings)
|
||||
.ThenInclude(r => r.EventDefinition)
|
||||
.OrderBy(s => s.LastName)
|
||||
.ThenBy(s => s.FirstName)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -79,7 +81,8 @@ public class NotePrintService : INotePrintService
|
||||
template,
|
||||
row.Imported,
|
||||
StudentTokens(row.Student),
|
||||
chapter))
|
||||
chapter,
|
||||
StudentHtmlFragments(row.Student)))
|
||||
];
|
||||
}
|
||||
|
||||
@@ -140,9 +143,25 @@ public class NotePrintService : INotePrintService
|
||||
.OrderBy(e => e.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var rankings = await _context.StudentEventRanking
|
||||
.AsNoTracking()
|
||||
.Include(r => r.Student)
|
||||
.Include(r => r.EventDefinition)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var rankingsByEventId = rankings
|
||||
.GroupBy(r => r.EventDefinition.Id)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
return
|
||||
[
|
||||
.. events.Select(evt => MergePage(evt.Name, template, null, EventTokens(evt), chapter))
|
||||
.. events.Select(evt => MergePage(
|
||||
evt.Name,
|
||||
template,
|
||||
null,
|
||||
EventTokens(evt),
|
||||
chapter,
|
||||
EventHtmlFragments(rankingsByEventId.GetValueOrDefault(evt.Id))))
|
||||
];
|
||||
}
|
||||
|
||||
@@ -151,16 +170,31 @@ public class NotePrintService : INotePrintService
|
||||
string template,
|
||||
Dictionary<string, string?>? imported,
|
||||
Dictionary<string, string?> entity,
|
||||
Dictionary<string, string?> chapter)
|
||||
Dictionary<string, string?> chapter,
|
||||
IReadOnlyDictionary<string, string>? htmlFragments = null)
|
||||
{
|
||||
var map = PrintTokenMap.Build(imported, entity, chapter);
|
||||
return new NotePrintPage
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Html = NoteTemplateMerger.ApplyLayout(MarkdownHelper.ToHtml(NoteTemplateMerger.Merge(template, map)))
|
||||
Html = NoteTemplateMerger.ApplyLayout(
|
||||
MarkdownHelper.ToHtml(NoteTemplateMerger.Merge(template, map)),
|
||||
htmlFragments)
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> StudentHtmlFragments(Student student) =>
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[NoteTemplateMerger.RankedEventsToken] = PrintRankBadgeHtml.ForStudentEvents(student.EventRankings)
|
||||
};
|
||||
|
||||
private static Dictionary<string, string> EventHtmlFragments(IReadOnlyList<StudentEventRanking>? rankings) =>
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
[NoteTemplateMerger.RankedStudentsToken] = PrintRankBadgeHtml.ForEventStudents(rankings)
|
||||
};
|
||||
|
||||
private Dictionary<string, string?> ChapterTokens()
|
||||
{
|
||||
var settings = ChapterSettings.FromConfiguration(_configuration);
|
||||
@@ -174,8 +208,9 @@ public class NotePrintService : INotePrintService
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> StudentTokens(Student student) =>
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
private static Dictionary<string, string?> StudentTokens(Student student)
|
||||
{
|
||||
var tokens = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["FirstName"] = student.FirstName,
|
||||
["LastName"] = student.LastName,
|
||||
@@ -191,6 +226,12 @@ public class NotePrintService : INotePrintService
|
||||
["OfficerRole"] = student.OfficerRole?.ToString()
|
||||
};
|
||||
|
||||
foreach (var (key, value) in StudentRankTokens.FromRankings(student.EventRankings))
|
||||
tokens[key] = value;
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string?> TeamTokens(Team team)
|
||||
{
|
||||
var evt = team.Event;
|
||||
@@ -222,7 +263,8 @@ public class NotePrintService : INotePrintService
|
||||
["TeamSize"] = evt?.TeamSize,
|
||||
["Eligibility"] = evt?.Eligibility,
|
||||
["Description"] = evt?.Description,
|
||||
["Theme"] = evt?.Theme
|
||||
["Theme"] = evt?.Theme,
|
||||
["EventAttributes"] = EventAttributeMarks.For(evt)
|
||||
};
|
||||
|
||||
private static Dictionary<string, string?> ImportedTokens(
|
||||
|
||||
+50
-2
@@ -61,6 +61,7 @@
|
||||
}
|
||||
|
||||
.print-answer-space {
|
||||
height: calc(var(--print-answer-lines, 3) * 1.35em);
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
@@ -88,6 +89,10 @@
|
||||
.event-rank-4 { background-color: #ffe599; }
|
||||
.event-rank-5 { background-color: #fff2cc; }
|
||||
.event-rank-6 { background-color: #fffaea; }
|
||||
.event-rank-7 { background-color: #fffefa; }
|
||||
.event-rank-8 { background-color: #fffefc; }
|
||||
.event-rank-9 { background-color: #fffffd; }
|
||||
.event-rank-10 { background-color: #fffffe; }
|
||||
|
||||
|
||||
.pre-wrap-text {
|
||||
@@ -390,9 +395,52 @@
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.print-rank-badges {
|
||||
margin: 0.2em 0 0.5em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.print-rank-badge {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.print-rank-dot {
|
||||
display: inline-block;
|
||||
width: 0.65em;
|
||||
height: 0.65em;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.print-event-attrs {
|
||||
font-family: monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.print-badge-legend {
|
||||
margin: 0.4em 0 0.9em;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.print-attr-legend {
|
||||
margin-top: 0.25em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.print-legend-mark {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.print-answer-space {
|
||||
min-height: calc(var(--print-answer-lines, 3) * 1.35em);
|
||||
margin: 0.2em 0 0.75em;
|
||||
display: block;
|
||||
height: calc(var(--print-answer-lines, 3) * 1.35em);
|
||||
overflow: hidden;
|
||||
margin: 0.35em 0 0.75em;
|
||||
box-sizing: content-box;
|
||||
background-image: repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent,
|
||||
|
||||
Reference in New Issue
Block a user