Refactor event occurrence parsing to unify section header handling and event count tracking

This commit updates the EventOccurrenceParseResult and EventOccurrenceParserResult classes to consolidate the handling of skipped section headers and event counts into a single set of properties. The previous separate lists and counts for middle school and high school sections have been replaced with a unified approach, improving clarity and maintainability. Additionally, the EventOccurrenceParserService has been modified to reflect these changes, ensuring consistent behavior across the application. This refactor enhances the overall structure of the event parsing logic.
This commit is contained in:
2026-01-10 18:19:16 -05:00
parent b7e812bb63
commit ecd6173a44
11 changed files with 528 additions and 472 deletions
+6 -15
View File
@@ -31,26 +31,17 @@ public class EventOccurrenceParseResult
public List<ParsingIssue> Issues { get; set; } = new(); public List<ParsingIssue> Issues { get; set; } = new();
/// <summary> /// <summary>
/// List of high school (HS) section headers that were encountered but skipped /// List of section headers that were encountered but skipped
/// because they don't match the chapter's school level setting. /// because they don't match the chapter's school level setting.
/// Headers may contain " - MS" or " - HS" to indicate school level.
/// </summary> /// </summary>
public List<string> SkippedHSSectionHeaders { get; set; } = new(); public List<string> SkippedSectionHeaders { get; set; } = new();
/// <summary> /// <summary>
/// List of middle school (MS) section headers that were encountered but skipped /// Number of event occurrences that were skipped due to school level filtering.
/// because they don't match the chapter's school level setting. /// This includes both MS and HS events that don't match the chapter's school level setting.
/// </summary> /// </summary>
public List<string> SkippedMSSectionHeaders { get; set; } = new(); public int SkippedEventCount { get; set; }
/// <summary>
/// Number of Middle School (MS) event occurrences that were skipped due to school level filtering.
/// </summary>
public int SkippedMSEventCount { get; set; }
/// <summary>
/// Number of High School (HS) event occurrences that were skipped due to school level filtering.
/// </summary>
public int SkippedHSEventCount { get; set; }
/// <summary> /// <summary>
/// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes. /// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes.
+6 -8
View File
@@ -14,10 +14,8 @@ public class EventOccurrenceParserResult
{ {
public IDictionary<EventDefinition, List<Entities.EventOccurrence>> Occurrences { get; set; } = new Dictionary<EventDefinition, List<Entities.EventOccurrence>>(); public IDictionary<EventDefinition, List<Entities.EventOccurrence>> Occurrences { get; set; } = new Dictionary<EventDefinition, List<Entities.EventOccurrence>>();
public List<ParsingIssue> Issues { get; set; } = new(); public List<ParsingIssue> Issues { get; set; } = new();
public List<string> SkippedHSSectionHeaders { get; set; } = new(); public List<string> SkippedSectionHeaders { get; set; } = new();
public List<string> SkippedMSSectionHeaders { get; set; } = new(); public int SkippedEventCount { get; set; }
public int SkippedMSEventCount { get; set; }
public int SkippedHSEventCount { get; set; }
/// <summary> /// <summary>
/// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes. /// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes.
/// Multiple footnotes are concatenated into a single string. /// Multiple footnotes are concatenated into a single string.
@@ -329,12 +327,12 @@ public class EventOccurrenceParser
// School level is set - filter based on it // School level is set - filter based on it
if (_schoolLevel.Value == SchoolLevel.MiddleSchool && sectionSchoolLevel.Value == SchoolLevel.HighSchool) if (_schoolLevel.Value == SchoolLevel.MiddleSchool && sectionSchoolLevel.Value == SchoolLevel.HighSchool)
{ {
result.SkippedHSSectionHeaders.Add(normalizedLine); result.SkippedSectionHeaders.Add(normalizedLine);
return true; return true;
} }
if (_schoolLevel.Value == SchoolLevel.HighSchool && sectionSchoolLevel.Value == SchoolLevel.MiddleSchool) if (_schoolLevel.Value == SchoolLevel.HighSchool && sectionSchoolLevel.Value == SchoolLevel.MiddleSchool)
{ {
result.SkippedMSSectionHeaders.Add(normalizedLine); result.SkippedSectionHeaders.Add(normalizedLine);
return true; return true;
} }
@@ -379,12 +377,12 @@ public class EventOccurrenceParser
// School level is set - filter based on it // School level is set - filter based on it
if (_schoolLevel.Value == SchoolLevel.MiddleSchool && currentSectionLevel.Value == SchoolLevel.HighSchool) if (_schoolLevel.Value == SchoolLevel.MiddleSchool && currentSectionLevel.Value == SchoolLevel.HighSchool)
{ {
result.SkippedHSEventCount++; result.SkippedEventCount++;
return true; return true;
} }
if (_schoolLevel.Value == SchoolLevel.HighSchool && currentSectionLevel.Value == SchoolLevel.MiddleSchool) if (_schoolLevel.Value == SchoolLevel.HighSchool && currentSectionLevel.Value == SchoolLevel.MiddleSchool)
{ {
result.SkippedMSEventCount++; result.SkippedEventCount++;
return true; return true;
} }
+5 -11
View File
@@ -110,10 +110,8 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
result.Issues.AddRange(parserResult.Issues); result.Issues.AddRange(parserResult.Issues);
// Copy skipped section headers from parser result // Copy skipped section headers from parser result
result.SkippedHSSectionHeaders.AddRange(parserResult.SkippedHSSectionHeaders); result.SkippedSectionHeaders.AddRange(parserResult.SkippedSectionHeaders);
result.SkippedMSSectionHeaders.AddRange(parserResult.SkippedMSSectionHeaders); result.SkippedEventCount = parserResult.SkippedEventCount;
result.SkippedMSEventCount = parserResult.SkippedMSEventCount;
result.SkippedHSEventCount = parserResult.SkippedHSEventCount;
// Copy footnotes from parser result // Copy footnotes from parser result
foreach (var kvp in parserResult.Footnotes) foreach (var kvp in parserResult.Footnotes)
@@ -121,14 +119,10 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
result.Footnotes[kvp.Key] = kvp.Value; result.Footnotes[kvp.Key] = kvp.Value;
} }
// Add informational messages about skipped events // Add informational message about skipped events
if (parserResult.SkippedMSEventCount > 0) if (parserResult.SkippedEventCount > 0)
{ {
result.Warnings.Add($"Skipped {parserResult.SkippedMSEventCount} Middle School (MS) event occurrence(s) based on school level setting"); result.Warnings.Add($"Skipped {parserResult.SkippedEventCount} event occurrence(s) from other school level based on school level setting");
}
if (parserResult.SkippedHSEventCount > 0)
{
result.Warnings.Add($"Skipped {parserResult.SkippedHSEventCount} High School (HS) event occurrence(s) based on school level setting");
} }
// Validate locations and add warnings for problematic ones // Validate locations and add warnings for problematic ones
+225 -347
View File
@@ -9,6 +9,16 @@ namespace Tests.Parsers;
/// </summary> /// </summary>
public class EventOccurrenceParser_Tests public class EventOccurrenceParser_Tests
{ {
#region Constants
private const int MaxLookbackLines = 20;
private const int TopUnmatchedPatternsCount = 10;
private const int SampleIssuesCount = 5;
private const int SectionTestStartLineIndex = 63; // Line 64 (1-based) = index 63 (0-based)
private const int SectionTestLineCount = 29; // Lines 64-92 inclusive
#endregion
#region Helper Methods #region Helper Methods
/// <summary> /// <summary>
@@ -41,20 +51,18 @@ public class EventOccurrenceParser_Tests
return true; return true;
// For MissingEventDefinition issues, check if we're in an HS section // For MissingEventDefinition issues, check if we're in an HS section
if (issue.IssueType == ParsingIssueType.MissingEventDefinition) if (issue.IssueType != ParsingIssueType.MissingEventDefinition) return false;
// Look backwards to find the most recent section header
for (int i = currentLineIndex - 1; i >= 0 && i >= currentLineIndex - MaxLookbackLines; i--)
{ {
// Look backwards to find the most recent section header if (i >= fileLines.Count) continue;
for (int i = currentLineIndex - 1; i >= 0 && i >= currentLineIndex - 20; i--)
{ var line = fileLines[i].Trim();
if (i < fileLines.Count) if (IsHighSchoolEvent(line))
{ return true;
var line = fileLines[i].Trim(); if (IsMiddleSchoolEvent(line))
if (IsHighSchoolEvent(line)) return false; // Found MS section, so this is fixable
return true;
if (IsMiddleSchoolEvent(line))
return false; // Found MS section, so this is fixable
}
}
} }
return false; return false;
@@ -88,7 +96,7 @@ public class EventOccurrenceParser_Tests
/// <summary> /// <summary>
/// Gets sample lines for a list of issues. /// Gets sample lines for a list of issues.
/// </summary> /// </summary>
private static List<string> GetSampleLines(List<ParsingIssue> issues, int count = 5) private static List<string> GetSampleLines(List<ParsingIssue> issues, int count = SampleIssuesCount)
{ {
return issues return issues
.Take(count) .Take(count)
@@ -96,6 +104,115 @@ public class EventOccurrenceParser_Tests
.ToList(); .ToList();
} }
/// <summary>
/// Writes special events summary to console.
/// </summary>
private static void WriteSpecialEventsSummary(IDictionary<EventDefinition, List<Core.Entities.EventOccurrence>> occurrences)
{
Console.WriteLine($"\n--- Special Events Found ---");
if (occurrences.TryGetValue(EventDefinition.GeneralSchedule, out var gs))
Console.WriteLine($" GeneralSchedule: {gs.Count} occurrences");
if (occurrences.TryGetValue(EventDefinition.MeetTheCandidates, out var mtc))
Console.WriteLine($" MeetTheCandidates: {mtc.Count} occurrences");
if (occurrences.TryGetValue(EventDefinition.ChapterOfficerMeeting, out var com))
Console.WriteLine($" ChapterOfficerMeeting: {com.Count} occurrences");
if (occurrences.TryGetValue(EventDefinition.VotingDelegateMeeting, out var vdm))
Console.WriteLine($" VotingDelegateMeeting: {vdm.Count} occurrences");
}
/// <summary>
/// Writes issue breakdown by type to console.
/// </summary>
private static void WriteIssueBreakdown(
Dictionary<ParsingIssueType, List<ParsingIssue>> issuesByType,
Dictionary<ParsingIssueType, int> expectedByType,
Dictionary<ParsingIssueType, int> fixableByType,
List<ParsingIssue> fixableIssues)
{
Console.WriteLine($"\n--- Issue Breakdown by Type ---");
foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count))
{
var issueType = kvp.Key;
var allIssues = kvp.Value;
expectedByType.TryGetValue(issueType, out var expectedCount);
fixableByType.TryGetValue(issueType, out var fixableCount);
Console.WriteLine($"\n{issueType}:");
Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})");
if (fixableCount > 0)
{
var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList();
var samples = GetSampleLines(fixableOfType, SampleIssuesCount);
Console.WriteLine($" Sample fixable issues:");
foreach (var sample in samples)
{
Console.WriteLine(sample);
}
}
}
}
/// <summary>
/// Writes unmatched line analysis to console.
/// </summary>
private static void WriteUnmatchedLineAnalysis(List<ParsingIssue> fixableIssues)
{
var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList();
if (!unmatchedLines.Any()) return;
Console.WriteLine($"\n--- Unmatched Line Analysis ---");
var unmatchedPatterns = unmatchedLines
.GroupBy(i => i.LineContent.Trim())
.OrderByDescending(g => g.Count())
.Take(TopUnmatchedPatternsCount);
Console.WriteLine($"Top unmatched line formats:");
foreach (var pattern in unmatchedPatterns)
{
Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)");
}
}
/// <summary>
/// Analyzes and reports parsing results for a file.
/// </summary>
private static void AnalyzeParsingResults(
EventOccurrenceParserResult result,
FileInfo fileInfo,
string title)
{
// Load file lines for analysis
var fileLines = File.ReadAllLines(fileInfo.FullName).ToList();
var totalLines = fileLines.Count;
var totalParsed = result.Occurrences.Values.Sum(list => list.Count);
// Categorize issues
var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
// Count event sections
var (hsSections, msSections) = CountEventSections(fileLines);
// Group issues by type
var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList());
var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
// Output analysis
Console.WriteLine($"\n=== {title} ===");
Console.WriteLine($"\n--- Summary Statistics ---");
Console.WriteLine($"Total lines in file: {totalLines}");
Console.WriteLine($"Total occurrences parsed: {totalParsed}");
Console.WriteLine($"Total issues found: {result.Issues.Count}");
Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}");
Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}");
WriteSpecialEventsSummary(result.Occurrences);
WriteIssueBreakdown(issuesByType, expectedByType, fixableByType, fixableIssues);
WriteUnmatchedLineAnalysis(fixableIssues);
}
/// <summary> /// <summary>
/// Counts HS vs MS event sections in the file. /// Counts HS vs MS event sections in the file.
@@ -117,68 +234,77 @@ public class EventOccurrenceParser_Tests
return (hsCount, msCount); return (hsCount, msCount);
} }
/// <summary>
/// Writes special events to console output.
/// </summary>
private static void WriteSpecialEvents(IDictionary<EventDefinition, List<Core.Entities.EventOccurrence>> occurrences)
{
Console.WriteLine("General Schedule");
if (occurrences.TryGetValue(EventDefinition.GeneralSchedule, out var generalSchedule))
{
foreach (var eo in generalSchedule.OrderBy(occurrence => occurrence.StartTime))
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Meet the Candidates");
if (occurrences.TryGetValue(EventDefinition.MeetTheCandidates, out var meetTheCandidates))
{
foreach (var eo in meetTheCandidates)
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Chapter Officer Meeting");
if (occurrences.TryGetValue(EventDefinition.ChapterOfficerMeeting, out var chapterOfficerMeeting))
{
foreach (var eo in chapterOfficerMeeting)
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Voting Delegate Meeting");
if (occurrences.TryGetValue(EventDefinition.VotingDelegateMeeting, out var votingDelegateMeeting))
{
foreach (var eo in votingDelegateMeeting)
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
}
#endregion #endregion
[Test] [Test]
public void ParseNationalsTest() public void ParseNationalsTest()
{ {
var events = TestEntityHandler.GetEvents(); var events = TestEntityHandler.GetEvents();
var parser = new EventOccurrenceParser(TestEntityHandler.GetEventOccurrenceNationalsFileInfo(), events); var parser = new EventOccurrenceParser(TestEntityHandler.GetEventOccurrenceNationalsFileInfo(), events);
var result = parser.Parse(); var result = parser.Parse();
var dictionary = result.Occurrences; var dictionary = result.Occurrences;
Console.WriteLine($"Occurrence, Month, Date, Time, Location"); Console.WriteLine($"Occurrence, Month, Date, Time, Location");
foreach (var @event in events) foreach (var @event in events)
{ {
Console.WriteLine($"{@event.Name}"); Console.WriteLine($"{@event.Name}");
if (!dictionary.ContainsKey(@event)) if (!dictionary.TryGetValue(@event, out var eventOccurrences))
{ {
Console.WriteLine("!!! eventDefinition not found " + @event.Name); Console.WriteLine($"!!! eventDefinition not found {@event.Name}");
continue; continue;
} }
var eventOccurrences = dictionary[@event];
foreach (var eo in eventOccurrences)
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("General Schedule"); foreach (var eo in eventOccurrences)
if (dictionary.ContainsKey(EventDefinition.GeneralSchedule)) {
{ Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
foreach (var eo in dictionary[EventDefinition.GeneralSchedule].OrderBy(occurrence => occurrence.StartTime)) }
{ }
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Meet the Candidates"); WriteSpecialEvents(dictionary);
if (dictionary.ContainsKey(EventDefinition.MeetTheCandidates))
{
foreach (var eo in dictionary[EventDefinition.MeetTheCandidates])
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Chapter Officer Meeting"); Assert.Pass();
if (dictionary.ContainsKey(EventDefinition.ChapterOfficerMeeting))
{
foreach (var eo in dictionary[EventDefinition.ChapterOfficerMeeting])
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Voting Delegate Meeting");
if (dictionary.ContainsKey(EventDefinition.VotingDelegateMeeting))
{
foreach (var eo in dictionary[EventDefinition.VotingDelegateMeeting])
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Assert.Pass();
} }
@@ -194,53 +320,19 @@ public class EventOccurrenceParser_Tests
{ {
Console.WriteLine($"{@event.Name}"); Console.WriteLine($"{@event.Name}");
if (!dictionary.ContainsKey(@event)) if (!dictionary.TryGetValue(@event, out var eventOccurrences))
{ {
Console.WriteLine("!!! eventDefinition not found " + @event.Name); Console.WriteLine($"!!! eventDefinition not found {@event.Name}");
continue; continue;
} }
var eventOccurrences = dictionary[@event];
foreach (var eo in eventOccurrences) foreach (var eo in eventOccurrences)
{ {
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}"); Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
} }
} }
Console.WriteLine("General Schedule"); WriteSpecialEvents(dictionary);
if (dictionary.ContainsKey(EventDefinition.GeneralSchedule))
{
foreach (var eo in dictionary[EventDefinition.GeneralSchedule].OrderBy(occurrence => occurrence.StartTime))
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Meet the Candidates");
if (dictionary.ContainsKey(EventDefinition.MeetTheCandidates))
{
foreach (var eo in dictionary[EventDefinition.MeetTheCandidates])
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Chapter Officer Meeting");
if (dictionary.ContainsKey(EventDefinition.ChapterOfficerMeeting))
{
foreach (var eo in dictionary[EventDefinition.ChapterOfficerMeeting])
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Console.WriteLine("Voting Delegate Meeting");
if (dictionary.ContainsKey(EventDefinition.VotingDelegateMeeting))
{
foreach (var eo in dictionary[EventDefinition.VotingDelegateMeeting])
{
Console.WriteLine($"\t{eo.StartTime.DayOfWeek} {eo.Time}, {eo.Name}, {eo.Location}");
}
}
Assert.Pass(); Assert.Pass();
} }
@@ -259,82 +351,11 @@ public class EventOccurrenceParser_Tests
// Assert - Should parse without exceptions // Assert - Should parse without exceptions
Assert.That(result, Is.Not.Null, "Parser should return a result"); Assert.That(result, Is.Not.Null, "Parser should return a result");
// Load file lines for analysis AnalyzeParsingResults(result, fileInfo, "2025 TSA Nationals Competition Event Times Analysis");
var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var fileLines = File.ReadAllLines(fileInfo.FullName).ToList();
var totalLines = fileLines.Count; var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
var totalParsed = result.Occurrences.Values.Sum(list => list.Count); var totalParsed = result.Occurrences.Values.Sum(list => list.Count);
// Categorize issues
var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
// Count event sections
var (hsSections, msSections) = CountEventSections(fileLines);
// Group issues by type
var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList());
var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
// Output analysis
Console.WriteLine($"\n=== 2025 TSA Nationals Competition Event Times Analysis ===");
Console.WriteLine($"\n--- Summary Statistics ---");
Console.WriteLine($"Total lines in file: {totalLines}");
Console.WriteLine($"Total occurrences parsed: {totalParsed}");
Console.WriteLine($"Total issues found: {result.Issues.Count}");
Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}");
Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}");
Console.WriteLine($"\n--- Special Events Found ---");
if (result.Occurrences.ContainsKey(EventDefinition.GeneralSchedule))
Console.WriteLine($" GeneralSchedule: {result.Occurrences[EventDefinition.GeneralSchedule].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.MeetTheCandidates))
Console.WriteLine($" MeetTheCandidates: {result.Occurrences[EventDefinition.MeetTheCandidates].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.ChapterOfficerMeeting))
Console.WriteLine($" ChapterOfficerMeeting: {result.Occurrences[EventDefinition.ChapterOfficerMeeting].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.VotingDelegateMeeting))
Console.WriteLine($" VotingDelegateMeeting: {result.Occurrences[EventDefinition.VotingDelegateMeeting].Count} occurrences");
Console.WriteLine($"\n--- Issue Breakdown by Type ---");
foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count))
{
var issueType = kvp.Key;
var allIssues = kvp.Value;
expectedByType.TryGetValue(issueType, out var expectedCount);
fixableByType.TryGetValue(issueType, out var fixableCount);
Console.WriteLine($"\n{issueType}:");
Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})");
if (fixableCount > 0)
{
var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList();
var samples = GetSampleLines(fixableOfType, 5);
Console.WriteLine($" Sample fixable issues:");
foreach (var sample in samples)
{
Console.WriteLine(sample);
}
}
}
// Pattern Analysis - LocationParseFailure issues are no longer created (pattern matching removed)
var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList();
if (unmatchedLines.Any())
{
Console.WriteLine($"\n--- Unmatched Line Analysis ---");
var unmatchedPatterns = unmatchedLines
.GroupBy(i => i.LineContent.Trim())
.OrderByDescending(g => g.Count())
.Take(10);
Console.WriteLine($"Top unmatched line formats:");
foreach (var pattern in unmatchedPatterns)
{
Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)");
}
}
// Test passes if no exceptions were thrown // Test passes if no exceptions were thrown
Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)");
@@ -354,82 +375,11 @@ public class EventOccurrenceParser_Tests
// Assert - Should parse without exceptions // Assert - Should parse without exceptions
Assert.That(result, Is.Not.Null, "Parser should return a result"); Assert.That(result, Is.Not.Null, "Parser should return a result");
// Load file lines for analysis AnalyzeParsingResults(result, fileInfo, "2025 TN TSA State Competition Event Times Analysis");
var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var fileLines = File.ReadAllLines(fileInfo.FullName).ToList();
var totalLines = fileLines.Count; var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
var totalParsed = result.Occurrences.Values.Sum(list => list.Count); var totalParsed = result.Occurrences.Values.Sum(list => list.Count);
// Categorize issues
var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
// Count event sections
var (hsSections, msSections) = CountEventSections(fileLines);
// Group issues by type
var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList());
var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
// Output analysis
Console.WriteLine($"\n=== 2025 TN TSA State Competition Event Times Analysis ===");
Console.WriteLine($"\n--- Summary Statistics ---");
Console.WriteLine($"Total lines in file: {totalLines}");
Console.WriteLine($"Total occurrences parsed: {totalParsed}");
Console.WriteLine($"Total issues found: {result.Issues.Count}");
Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}");
Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}");
Console.WriteLine($"\n--- Special Events Found ---");
if (result.Occurrences.ContainsKey(EventDefinition.GeneralSchedule))
Console.WriteLine($" GeneralSchedule: {result.Occurrences[EventDefinition.GeneralSchedule].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.MeetTheCandidates))
Console.WriteLine($" MeetTheCandidates: {result.Occurrences[EventDefinition.MeetTheCandidates].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.ChapterOfficerMeeting))
Console.WriteLine($" ChapterOfficerMeeting: {result.Occurrences[EventDefinition.ChapterOfficerMeeting].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.VotingDelegateMeeting))
Console.WriteLine($" VotingDelegateMeeting: {result.Occurrences[EventDefinition.VotingDelegateMeeting].Count} occurrences");
Console.WriteLine($"\n--- Issue Breakdown by Type ---");
foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count))
{
var issueType = kvp.Key;
var allIssues = kvp.Value;
expectedByType.TryGetValue(issueType, out var expectedCount);
fixableByType.TryGetValue(issueType, out var fixableCount);
Console.WriteLine($"\n{issueType}:");
Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})");
if (fixableCount > 0)
{
var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList();
var samples = GetSampleLines(fixableOfType, 5);
Console.WriteLine($" Sample fixable issues:");
foreach (var sample in samples)
{
Console.WriteLine(sample);
}
}
}
// Pattern Analysis - LocationParseFailure issues are no longer created (pattern matching removed)
var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList();
if (unmatchedLines.Any())
{
Console.WriteLine($"\n--- Unmatched Line Analysis ---");
var unmatchedPatterns = unmatchedLines
.GroupBy(i => i.LineContent.Trim())
.OrderByDescending(g => g.Count())
.Take(10);
Console.WriteLine($"Top unmatched line formats:");
foreach (var pattern in unmatchedPatterns)
{
Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)");
}
}
// Test passes if no exceptions were thrown // Test passes if no exceptions were thrown
Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)");
@@ -449,82 +399,11 @@ public class EventOccurrenceParser_Tests
// Assert - Should parse without exceptions // Assert - Should parse without exceptions
Assert.That(result, Is.Not.Null, "Parser should return a result"); Assert.That(result, Is.Not.Null, "Parser should return a result");
// Load file lines for analysis AnalyzeParsingResults(result, fileInfo, "2024 TN TSA State Competition Event Times Analysis");
var fileLines = File.ReadAllLines(fileInfo.FullName).ToList(); var fileLines = File.ReadAllLines(fileInfo.FullName).ToList();
var totalLines = fileLines.Count; var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
var totalParsed = result.Occurrences.Values.Sum(list => list.Count); var totalParsed = result.Occurrences.Values.Sum(list => list.Count);
// Categorize issues
var (expectedIssues, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
// Count event sections
var (hsSections, msSections) = CountEventSections(fileLines);
// Group issues by type
var issuesByType = result.Issues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.ToList());
var expectedByType = expectedIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
var fixableByType = fixableIssues.GroupBy(i => i.IssueType).ToDictionary(g => g.Key, g => g.Count());
// Output analysis
Console.WriteLine($"\n=== 2024 TN TSA State Competition Event Times Analysis ===");
Console.WriteLine($"\n--- Summary Statistics ---");
Console.WriteLine($"Total lines in file: {totalLines}");
Console.WriteLine($"Total occurrences parsed: {totalParsed}");
Console.WriteLine($"Total issues found: {result.Issues.Count}");
Console.WriteLine($" Expected issues (HS-related): {expectedIssues.Count} ({100.0 * expectedIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($" Fixable issues: {fixableIssues.Count} ({100.0 * fixableIssues.Count / Math.Max(1, result.Issues.Count):F1}%)");
Console.WriteLine($"Event sections: HS={hsSections}, MS={msSections}");
Console.WriteLine($"Events with occurrences: {result.Occurrences.Count}");
Console.WriteLine($"\n--- Special Events Found ---");
if (result.Occurrences.ContainsKey(EventDefinition.GeneralSchedule))
Console.WriteLine($" GeneralSchedule: {result.Occurrences[EventDefinition.GeneralSchedule].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.MeetTheCandidates))
Console.WriteLine($" MeetTheCandidates: {result.Occurrences[EventDefinition.MeetTheCandidates].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.ChapterOfficerMeeting))
Console.WriteLine($" ChapterOfficerMeeting: {result.Occurrences[EventDefinition.ChapterOfficerMeeting].Count} occurrences");
if (result.Occurrences.ContainsKey(EventDefinition.VotingDelegateMeeting))
Console.WriteLine($" VotingDelegateMeeting: {result.Occurrences[EventDefinition.VotingDelegateMeeting].Count} occurrences");
Console.WriteLine($"\n--- Issue Breakdown by Type ---");
foreach (var kvp in issuesByType.OrderByDescending(x => x.Value.Count))
{
var issueType = kvp.Key;
var allIssues = kvp.Value;
expectedByType.TryGetValue(issueType, out var expectedCount);
fixableByType.TryGetValue(issueType, out var fixableCount);
Console.WriteLine($"\n{issueType}:");
Console.WriteLine($" Total: {allIssues.Count} (Expected: {expectedCount}, Fixable: {fixableCount})");
if (fixableCount > 0)
{
var fixableOfType = fixableIssues.Where(i => i.IssueType == issueType).ToList();
var samples = GetSampleLines(fixableOfType, 5);
Console.WriteLine($" Sample fixable issues:");
foreach (var sample in samples)
{
Console.WriteLine(sample);
}
}
}
// Pattern Analysis - LocationParseFailure issues are no longer created (pattern matching removed)
var unmatchedLines = fixableIssues.Where(i => i.IssueType == ParsingIssueType.UnmatchedLine).ToList();
if (unmatchedLines.Any())
{
Console.WriteLine($"\n--- Unmatched Line Analysis ---");
var unmatchedPatterns = unmatchedLines
.GroupBy(i => i.LineContent.Trim())
.OrderByDescending(g => g.Count())
.Take(10);
Console.WriteLine($"Top unmatched line formats:");
foreach (var pattern in unmatchedPatterns)
{
Console.WriteLine($" \"{pattern.Key}\" (appears {pattern.Count()} times)");
}
}
// Test passes if no exceptions were thrown // Test passes if no exceptions were thrown
Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)"); Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)");
@@ -536,7 +415,7 @@ public class EventOccurrenceParser_Tests
// Arrange // Arrange
// Extract lines 64-92 from the test file - contains MS and HS events with various formats // Extract lines 64-92 from the test file - contains MS and HS events with various formats
var allLines = File.ReadAllLines(TestEntityHandler.GetEventOccurrenceStateFileInfo().FullName); var allLines = File.ReadAllLines(TestEntityHandler.GetEventOccurrenceStateFileInfo().FullName);
var sectionLines = allLines.Skip(63).Take(29).ToArray(); // Lines 64-92 (0-indexed: 63-91) var sectionLines = allLines.Skip(SectionTestStartLineIndex).Take(SectionTestLineCount).ToArray(); // Lines 64-92 (0-indexed: 63-91)
var sectionContent = string.Join("\n", sectionLines); var sectionContent = string.Join("\n", sectionLines);
var tempFile = EventOccurrenceParserTestHelpers.CreateTempFile(sectionContent); var tempFile = EventOccurrenceParserTestHelpers.CreateTempFile(sectionContent);
@@ -568,14 +447,14 @@ public class EventOccurrenceParser_Tests
// Total expected MS occurrences: 16 // Total expected MS occurrences: 16
var msEventCount = 0; var msEventCount = 0;
if (childrensStories != null && result.Occurrences.ContainsKey(childrensStories)) if (childrensStories != null && result.Occurrences.TryGetValue(childrensStories, out var csOccurrences))
msEventCount += result.Occurrences[childrensStories].Count; msEventCount += csOccurrences.Count;
if (coding != null && result.Occurrences.ContainsKey(coding)) if (coding != null && result.Occurrences.TryGetValue(coding, out var codingOccurrences))
msEventCount += result.Occurrences[coding].Count; msEventCount += codingOccurrences.Count;
if (communityServiceVideo != null && result.Occurrences.ContainsKey(communityServiceVideo)) if (communityServiceVideo != null && result.Occurrences.TryGetValue(communityServiceVideo, out var csvOccurrences))
msEventCount += result.Occurrences[communityServiceVideo].Count; msEventCount += csvOccurrences.Count;
if (constructionChallenge != null && result.Occurrences.ContainsKey(constructionChallenge)) if (constructionChallenge != null && result.Occurrences.TryGetValue(constructionChallenge, out var ccOccurrences))
msEventCount += result.Occurrences[constructionChallenge].Count; msEventCount += ccOccurrences.Count;
// When no school level is set, HS events should be processed (not skipped) // When no school level is set, HS events should be processed (not skipped)
// Verify HS events are processed or handled appropriately // Verify HS events are processed or handled appropriately
@@ -585,8 +464,8 @@ public class EventOccurrenceParser_Tests
i.LineNumber >= 72 && i.LineNumber <= 86 && IsHighSchoolEvent(i.LineContent) i.LineNumber >= 72 && i.LineNumber <= 86 && IsHighSchoolEvent(i.LineContent)
).ToList(); ).ToList();
// Verify HS section headers are NOT tracked in SkippedHSSectionHeaders when no school level is set // Verify HS section headers are NOT tracked in SkippedSectionHeaders when no school level is set
var skippedHSHeaders = result.SkippedHSSectionHeaders; var skippedHeaders = result.SkippedSectionHeaders;
// Verify continuation lines are skipped // Verify continuation lines are skipped
// Line 70 starts with "*The" - this enters continuation mode and both line 70 and 71 should be skipped // Line 70 starts with "*The" - this enters continuation mode and both line 70 and 71 should be skipped
@@ -610,7 +489,7 @@ public class EventOccurrenceParser_Tests
Console.WriteLine($"MS event occurrences: {msEventCount}"); Console.WriteLine($"MS event occurrences: {msEventCount}");
Console.WriteLine($"Total issues: {result.Issues.Count}"); Console.WriteLine($"Total issues: {result.Issues.Count}");
Console.WriteLine($"HS-related issues: {hsIssues.Count}"); Console.WriteLine($"HS-related issues: {hsIssues.Count}");
Console.WriteLine($"Skipped HS section headers: {skippedHSHeaders.Count}"); Console.WriteLine($"Skipped section headers: {skippedHeaders.Count}");
Console.WriteLine($"Continuation line issues: {continuationLineIssues.Count}"); Console.WriteLine($"Continuation line issues: {continuationLineIssues.Count}");
Console.WriteLine($"\n--- Issue Types ---"); Console.WriteLine($"\n--- Issue Types ---");
@@ -625,7 +504,7 @@ public class EventOccurrenceParser_Tests
// When no school level is set, HS events should be processed (not skipped) // When no school level is set, HS events should be processed (not skipped)
// HS events may create issues if they don't match event definitions, which is expected // HS events may create issues if they don't match event definitions, which is expected
// HS section headers should NOT be tracked when no school level is set // HS section headers should NOT be tracked when no school level is set
Assert.That(skippedHSHeaders, Has.Count.EqualTo(0), "HS section headers should NOT be tracked when no school level is set"); Assert.That(skippedHeaders, Has.Count.EqualTo(0), "Section headers should NOT be tracked when no school level is set");
// Line 70 (starts with "*The") enters continuation mode and both line 70 and 71 should be skipped without issues // Line 70 (starts with "*The") enters continuation mode and both line 70 and 71 should be skipped without issues
Assert.That(continuationLineIssues, Has.Count.EqualTo(0), Assert.That(continuationLineIssues, Has.Count.EqualTo(0),
"Continuation lines starting with '*' and subsequent lines should be skipped without issues"); "Continuation lines starting with '*' and subsequent lines should be skipped without issues");
@@ -633,9 +512,9 @@ public class EventOccurrenceParser_Tests
Assert.That(lateTimeOccurrence, Is.Not.Null, "Should parse 11:59 p.m. time format"); Assert.That(lateTimeOccurrence, Is.Not.Null, "Should parse 11:59 p.m. time format");
// Verify specific locations are parsed // Verify specific locations are parsed
if (childrensStories != null && result.Occurrences.ContainsKey(childrensStories)) if (childrensStories != null && result.Occurrences.TryGetValue(childrensStories, out var childrensStoriesOccurrences))
{ {
var locations = result.Occurrences[childrensStories] var locations = childrensStoriesOccurrences
.Select(eo => eo.Location) .Select(eo => eo.Location)
.Where(loc => !string.IsNullOrWhiteSpace(loc)) .Where(loc => !string.IsNullOrWhiteSpace(loc))
.ToList(); .ToList();
@@ -680,13 +559,12 @@ public class EventOccurrenceParser_Tests
// When no school level is set, all events (MS and HS) should be processed // When no school level is set, all events (MS and HS) should be processed
// HS section header should NOT be skipped (note: normalized to regular hyphen) // HS section header should NOT be skipped (note: normalized to regular hyphen)
Assert.That(result.SkippedHSSectionHeaders, Does.Not.Contain("Biotechnology Design - HS"), Assert.That(result.SkippedSectionHeaders, Does.Not.Contain("Biotechnology Design - HS"),
"HS section header should NOT be in SkippedHSSectionHeaders when no school level is set"); "HS section header should NOT be in SkippedSectionHeaders when no school level is set");
// With no school level filtering, both MS and HS events are processed // With no school level filtering, both MS and HS events are processed
if (result.Occurrences.ContainsKey(biotechnology)) if (result.Occurrences.TryGetValue(biotechnology, out var allOccurrences))
{ {
var allOccurrences = result.Occurrences[biotechnology];
// With no school level set, we process all occurrences (both MS and HS) // With no school level set, we process all occurrences (both MS and HS)
// Expected: 2 MS occurrences (Submit Entry, Judging) + 3 HS occurrences (Submit Entry, Judging, Pick-up) = 5 total // Expected: 2 MS occurrences (Submit Entry, Judging) + 3 HS occurrences (Submit Entry, Judging, Pick-up) = 5 total
Assert.That(allOccurrences, Has.Count.EqualTo(5), Assert.That(allOccurrences, Has.Count.EqualTo(5),
@@ -0,0 +1,79 @@
@using Core.Entities
@using MudBlazor
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Event Details</MudText>
</TitleContent>
<DialogContent>
@if (EventOccurrence == null)
{
<MudText>No event data available.</MudText>
}
else
{
<MudStack Spacing="3">
@if (EventDefinition != null)
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Event</MudText>
<MudText Typo="Typo.h6">@EventDefinition.Name</MudText>
</div>
}
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Occurrence</MudText>
<MudText Typo="Typo.body1">@EventOccurrence.Name</MudText>
</div>
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Date & Time</MudText>
<MudText Typo="Typo.body1">
@EventOccurrence.StartTime.ToString("f")
@if (EventOccurrence.EndTime.HasValue)
{
<text> - @EventOccurrence.EndTime.Value.ToString("t")</text>
}
</MudText>
</div>
@if (!string.IsNullOrEmpty(EventOccurrence.Location))
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Location</MudText>
<MudText Typo="Typo.body1">
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" Class="mr-1" />
@EventOccurrence.Location
</MudText>
</div>
}
@if (!string.IsNullOrEmpty(EventOccurrence.Time))
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Time</MudText>
<MudText Typo="Typo.body1">@EventOccurrence.Time</MudText>
</div>
}
@if (StudentFirstNames != null && StudentFirstNames.Any())
{
<div>
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Students</MudText>
<MudText Typo="Typo.body1">
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Small" Class="mr-1" />
@string.Join(", ", StudentFirstNames)
</MudText>
</div>
}
</MudStack>
}
</DialogContent>
</MudDialog>
@code {
[Parameter] public EventOccurrence? EventOccurrence { get; set; }
[Parameter] public EventDefinition? EventDefinition { get; set; }
[Parameter] public List<string>? StudentFirstNames { get; set; }
}
@@ -1,11 +1,8 @@
@page "/calendar/event-occurrences/import" @page "/calendar/event-occurrences/import"
@attribute [Authorize] @attribute [Authorize]
@using Core.Entities
@using Core.Models @using Core.Models
@using Core.Services @using Core.Services
@using Microsoft.EntityFrameworkCore @using Microsoft.EntityFrameworkCore
@using WebApp.Components.Shared.Components
@using MudBlazor
@inject IEventOccurrenceParserService ParserService @inject IEventOccurrenceParserService ParserService
@inject AppDbContext Context @inject AppDbContext Context
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@@ -125,47 +122,29 @@
{ {
<MudAlert Severity="Severity.Success" Dense="true"> <MudAlert Severity="Severity.Success" Dense="true">
Successfully parsed @_parseResult.TotalParsed occurrence(s) from @_parseResult.Occurrences.Count event definition(s) Successfully parsed @_parseResult.TotalParsed occurrence(s) from @_parseResult.Occurrences.Count event definition(s)
@if (_parseResult.SkippedMSEventCount > 0 || _parseResult.SkippedHSEventCount > 0) @if (_parseResult.SkippedEventCount > 0)
{ {
<text> (Skipped @(_parseResult.SkippedMSEventCount + _parseResult.SkippedHSEventCount) event occurrence(s) from other school level)</text> <text> (Skipped @_parseResult.SkippedEventCount event occurrence(s) from other school level)</text>
} }
</MudAlert> </MudAlert>
} }
@* Skipped Section Headers *@ @* Skipped Section Headers *@
@if ((_parseResult.SkippedMSSectionHeaders.Any() || _parseResult.SkippedHSSectionHeaders.Any()) && _parseResult.IsSuccess) @if (_parseResult.SkippedSectionHeaders.Any() && _parseResult.IsSuccess)
{ {
<MudExpansionPanels Elevation="0" Class="mt-2"> <MudExpansionPanels Elevation="0" Class="mt-2">
@if (_parseResult.SkippedMSSectionHeaders.Any()) <MudExpansionPanel Text="@($"Skipped Section Headers ({_parseResult.SkippedSectionHeaders.Count})")"
{ Icon="@Icons.Material.Filled.Info"
<MudExpansionPanel Text="@($"Skipped MS Section Headers ({_parseResult.SkippedMSSectionHeaders.Count})")" iconcolor="Color.Info">
Icon="@Icons.Material.Filled.Info" <MudList T="string">
iconcolor="Color.Info"> @foreach (var header in _parseResult.SkippedSectionHeaders)
<MudList T="string"> {
@foreach (var header in _parseResult.SkippedMSSectionHeaders) <MudListItem T="string">
{ <MudText>@header</MudText>
<MudListItem T="string"> </MudListItem>
<MudText>@header</MudText> }
</MudListItem> </MudList>
} </MudExpansionPanel>
</MudList>
</MudExpansionPanel>
}
@if (_parseResult.SkippedHSSectionHeaders.Any())
{
<MudExpansionPanel Text="@($"Skipped HS Section Headers ({_parseResult.SkippedHSSectionHeaders.Count})")"
Icon="@Icons.Material.Filled.Info"
iconcolor="Color.Info">
<MudList T="string">
@foreach (var header in _parseResult.SkippedHSSectionHeaders)
{
<MudListItem T="string">
<MudText>@header</MudText>
</MudListItem>
}
</MudList>
</MudExpansionPanel>
}
</MudExpansionPanels> </MudExpansionPanels>
} }
+154 -31
View File
@@ -1,12 +1,13 @@
@page "/calendar" @page "/calendar"
@attribute [Authorize] @attribute [Authorize]
@using WebApp.Components.Shared.Components
@using WebApp.Models @using WebApp.Models
@using WebApp.Services @using WebApp.Services
@using Heron.MudCalendar @using Heron.MudCalendar
@using Microsoft.Extensions.Logging @using Microsoft.Extensions.Logging
@using WebApp.Authentication
@inject IEventOccurrenceService EventOccurrenceService @inject IEventOccurrenceService EventOccurrenceService
@inject ILogger<Index> Logger @inject ILogger<Index> Logger
@inject IDialogService DialogService
<PageHeader Title="Event Calendar" Description="View competition schedules and event occurrences" Icon="@AppIcons.EventCalendar"> <PageHeader Title="Event Calendar" Description="View competition schedules and event occurrences" Icon="@AppIcons.EventCalendar">
<ActionButtons> <ActionButtons>
@@ -25,17 +26,53 @@
} }
else else
{ {
<MudCalendar T="CalendarEventItem" <MudStack Spacing="2">
Items="_calendarItems" <MudButtonGroup Variant="Variant.Outlined" Size="Size.Small">
View="CalendarView.Day" <MudButton Color="@(_currentView == CalendarView.Month ? Color.Primary : Color.Default)"
CurrentDay="@_calendarDate" OnClick="() => { _currentView = CalendarView.Month; StateHasChanged(); }">
/> Month
</MudButton>
<MudButton Color="@(_currentView == CalendarView.Day ? Color.Primary : Color.Default)"
OnClick="() => { _currentView = CalendarView.Day; StateHasChanged(); }">
Day
</MudButton>
</MudButtonGroup>
<MudCalendar T="CalendarEventItem"
Items="_calendarItems"
View="_currentView"
CurrentDay="@_calendarDate"
Class="event-calendar"
ItemClicked="OnItemClicked">
<MonthTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div class="d-flex gap-1">
<MudIcon Icon="@Icons.Material.Filled.Circle" Color="Color.Secondary" Size="Size.Small"/>
<div>@context.Text</div>
</div>
@* </MudTooltip> *@
</MonthTemplate>
<WeekTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div style="width: 100%; height: 100%;">
@context.Text
</div>
@* </MudTooltip> *@
</WeekTemplate>
<DayTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div>@context.Text</div>
@* </MudTooltip> *@
</DayTemplate>
</MudCalendar>
</MudStack>
} }
</MudPaper> </MudPaper>
@code { @code {
private List<CalendarEventItem>? _calendarItems; private List<CalendarEventItem>? _calendarItems;
private DateTime _calendarDate = DateTime.Today; private DateTime _calendarDate = DateTime.Today;
private CalendarView _currentView = CalendarView.Month;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -48,33 +85,34 @@
{ {
Logger.LogInformation("Loading calendar events"); Logger.LogInformation("Loading calendar events");
var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync(); var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync();
if (occurrences == null)
{
Logger.LogWarning("Service returned null occurrences");
_calendarItems = new List<CalendarEventItem>();
return;
}
Logger.LogDebug("Received {Count} occurrences from service", occurrences.Count()); var eventOccurrences = occurrences as EventOccurrence[] ?? occurrences.ToArray();
Logger.LogDebug("Received {Count} occurrences from service", eventOccurrences.Count());
// Get all unique event definition IDs that have occurrences
var eventDefinitionIds = eventOccurrences
.Where(occ => occ?.EventDefinition?.Id != null)
.Select(occ => occ!.EventDefinition!.Id)
.Distinct()
.ToList();
// Load teams for all event definitions
var teamsByEventId = await EventOccurrenceService.GetTeamsByEventDefinitionIdsAsync(eventDefinitionIds);
var items = new List<CalendarEventItem>(); var items = new List<CalendarEventItem>();
foreach (var occ in occurrences) foreach (var occ in eventOccurrences)
{ {
try try
{ {
if (occ == null)
{
Logger.LogWarning("Null occurrence found, skipping");
continue;
}
if (string.IsNullOrEmpty(occ.Name)) if (string.IsNullOrEmpty(occ.Name))
{ {
Logger.LogWarning("Occurrence with Id={Id} has null or empty Name", occ.Id); Logger.LogWarning("Occurrence with Id={Id} has null or empty Name", occ.Id);
} }
var calendarItem = new CalendarEventItem(occ, occ.EventDefinition); // Get student first names for this event definition
var studentFirstNames = StudentFirstNames(occ.EventDefinition, teamsByEventId);
var calendarItem = new CalendarEventItem(occ, studentFirstNames);
items.Add(calendarItem); items.Add(calendarItem);
} }
catch (Exception ex) catch (Exception ex)
@@ -87,7 +125,7 @@
_calendarItems = items; _calendarItems = items;
Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences", Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences",
_calendarItems.Count, occurrences.Count()); _calendarItems.Count, eventOccurrences.Count());
// Find the next date with events // Find the next date with events
_calendarDate = GetNextDateWithEvents(); _calendarDate = GetNextDateWithEvents();
@@ -103,6 +141,35 @@
} }
} }
private static List<string> StudentFirstNames(EventDefinition ed, Dictionary<int, List<Team>> teamsByEventId)
{
var studentFirstNames = new List<string>();
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() private DateTime GetNextDateWithEvents()
{ {
try try
@@ -119,7 +186,7 @@
{ {
try try
{ {
return item != null && item.Start.Date >= today; return item.Start.Date >= today;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -137,16 +204,10 @@
// Fallback to first event if no future events // Fallback to first event if no future events
var firstEvent = _calendarItems var firstEvent = _calendarItems
.Where(item => item != null)
.OrderBy(item => item.Start) .OrderBy(item => item.Start)
.FirstOrDefault(); .FirstOrDefault();
if (firstEvent != null) return firstEvent != null ? firstEvent.Start.Date : DateTime.Today;
{
return firstEvent.Start.Date;
}
return DateTime.Today;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -154,5 +215,67 @@
return DateTime.Today; return DateTime.Today;
} }
} }
private string GetEventTooltip(CalendarEventItem item)
{
var parts = new List<string>();
if (!string.IsNullOrEmpty(item.EventDefinition?.Name))
{
parts.Add($"Event: {item.EventDefinition.Name}");
}
if (!string.IsNullOrEmpty(item.EventOccurrenceData?.Name))
{
parts.Add($"Occurrence: {item.EventOccurrenceData.Name}");
}
if (!string.IsNullOrEmpty(item.EventOccurrenceData?.Location))
{
parts.Add($"Location: {item.EventOccurrenceData.Location}");
}
if (item.EventOccurrenceData?.StartTime != null)
{
parts.Add($"Time: {item.EventOccurrenceData.StartTime:g}");
}
if (item.StudentFirstNames.Any())
{
parts.Add($"Students: {string.Join(", ", item.StudentFirstNames)}");
}
return string.Join("\n", parts);
}
private async Task OnItemClicked(CalendarEventItem calendarEventItem)
{
if (_calendarItems == null)
return;
await ShowEventDetails(calendarEventItem);
}
private async Task ShowEventDetails(CalendarEventItem item)
{
if (item.EventOccurrenceData == null) return;
var parameters = new DialogParameters
{
["EventOccurrence"] = item.EventOccurrenceData,
["EventDefinition"] = item.EventDefinition,
["StudentFirstNames"] = item.StudentFirstNames
};
var options = new DialogOptions
{
CloseOnEscapeKey = true,
CloseButton = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
await DialogService.ShowAsync<EventOccurrenceDetailsDialog>("Event Details", parameters, options);
}
} }
@@ -55,7 +55,7 @@
</PropertyColumn> </PropertyColumn>
<PropertyColumn Property="@(e => e.Grade)" Title="Grade (TSA Year)" Sortable="true"> <PropertyColumn Property="@(e => e.Grade)" Title="Grade (TSA Year)" Sortable="true">
<CellTemplate> <CellTemplate>
@((MarkupString)AppIcons.GetOrdinalSuperscript(context.Item.Grade)) (@context.Item.TsaYear) <span style="white-space: nowrap;">@((MarkupString)AppIcons.GetOrdinalSuperscript(context.Item.Grade))</span> (@context.Item.TsaYear)
</CellTemplate> </CellTemplate>
</PropertyColumn> </PropertyColumn>
</Columns> </Columns>
+13 -23
View File
@@ -1,5 +1,5 @@
using Core.Entities;
using Heron.MudCalendar; using Heron.MudCalendar;
using MudBlazor;
namespace WebApp.Models; namespace WebApp.Models;
@@ -12,12 +12,17 @@ public class CalendarEventItem : CalendarItem
/// <summary> /// <summary>
/// Gets the original EventOccurrence data. /// Gets the original EventOccurrence data.
/// </summary> /// </summary>
public Core.Entities.EventOccurrence? EventOccurrenceData { get; set; } public EventOccurrence? EventOccurrenceData { get; set; }
/// <summary> /// <summary>
/// Gets the associated EventDefinition if available. /// Gets the associated EventDefinition if available.
/// </summary> /// </summary>
public Core.Entities.EventDefinition? EventDefinition { get; set; } public EventDefinition? EventDefinition { get; set; }
/// <summary>
/// Gets the list of student first names from teams matching the EventDefinition.
/// </summary>
public List<string> StudentFirstNames { get; set; } = [];
/// <summary> /// <summary>
/// Parameterless constructor required by Heron.MudCalendar component. /// Parameterless constructor required by Heron.MudCalendar component.
@@ -30,30 +35,15 @@ public class CalendarEventItem : CalendarItem
End = DateTime.MinValue; End = DateTime.MinValue;
} }
public CalendarEventItem(Core.Entities.EventOccurrence occurrence, Core.Entities.EventDefinition? eventDefinition = null) public CalendarEventItem(EventOccurrence occurrence, IEnumerable<string>? studentFirstNames = null)
{ {
EventOccurrenceData = occurrence;
EventDefinition = occurrence.EventDefinition;
// Set base class properties that the calendar component uses // Set base class properties that the calendar component uses
Text = GetEventTitle(occurrence, eventDefinition); StudentFirstNames = studentFirstNames?.ToList() ?? [];
Text = occurrence.EventDefinition?.ShortName;
Start = occurrence.StartTime; Start = occurrence.StartTime;
End = occurrence.EndTime ?? occurrence.StartTime.AddHours(1); End = occurrence.EndTime ?? occurrence.StartTime.AddHours(1);
this.EventOccurrenceData = occurrence;
this.EventDefinition = eventDefinition;
}
private static string GetEventTitle(Core.Entities.EventOccurrence occurrence, Core.Entities.EventDefinition? eventDefinition)
{
var title = occurrence.Name;
if (eventDefinition != null && !string.IsNullOrEmpty(eventDefinition.Name))
{
title = $"{eventDefinition.Name} - {title}";
}
if (!string.IsNullOrEmpty(occurrence.Location))
{
title += $" ({occurrence.Location})";
}
return title;
} }
} }
+19
View File
@@ -29,5 +29,24 @@ public class EventOccurrenceService : IEventOccurrenceService
.OrderBy(eo => eo.StartTime) .OrderBy(eo => eo.StartTime)
.ToListAsync(); .ToListAsync();
} }
public async Task<Dictionary<int, List<Team>>> GetTeamsByEventDefinitionIdsAsync(IEnumerable<int> eventDefinitionIds)
{
var ids = eventDefinitionIds.Where(id => id > 0).Distinct().ToList();
if (!ids.Any())
{
return new Dictionary<int, List<Team>>();
}
var teams = await _context.Teams
.Include(t => t.Students)
.Include(t => t.Captain)
.Where(t => ids.Contains(t.Event.Id))
.ToListAsync();
return teams
.GroupBy(t => t.Event.Id)
.ToDictionary(g => g.Key, g => g.ToList());
}
} }
@@ -13,5 +13,10 @@ public interface IEventOccurrenceService
/// Gets event occurrences within the specified date range. /// Gets event occurrences within the specified date range.
/// </summary> /// </summary>
Task<IEnumerable<EventOccurrence>> GetEventOccurrencesForDateRangeAsync(DateTime start, DateTime end); Task<IEnumerable<EventOccurrence>> GetEventOccurrencesForDateRangeAsync(DateTime start, DateTime end);
/// <summary>
/// Gets all teams for the specified event definition IDs, including students.
/// </summary>
Task<Dictionary<int, List<Team>>> GetTeamsByEventDefinitionIdsAsync(IEnumerable<int> eventDefinitionIds);
} }