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:
@@ -31,26 +31,17 @@ public class EventOccurrenceParseResult
|
||||
public List<ParsingIssue> Issues { get; set; } = new();
|
||||
|
||||
/// <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.
|
||||
/// Headers may contain " - MS" or " - HS" to indicate school level.
|
||||
/// </summary>
|
||||
public List<string> SkippedHSSectionHeaders { get; set; } = new();
|
||||
public List<string> SkippedSectionHeaders { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of middle school (MS) section headers that were encountered but skipped
|
||||
/// because they don't match the chapter's school level setting.
|
||||
/// Number of event occurrences that were skipped due to school level filtering.
|
||||
/// This includes both MS and HS events that don't match the chapter's school level setting.
|
||||
/// </summary>
|
||||
public List<string> SkippedMSSectionHeaders { get; set; } = new();
|
||||
|
||||
/// <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; }
|
||||
public int SkippedEventCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes.
|
||||
|
||||
@@ -14,10 +14,8 @@ public class EventOccurrenceParserResult
|
||||
{
|
||||
public IDictionary<EventDefinition, List<Entities.EventOccurrence>> Occurrences { get; set; } = new Dictionary<EventDefinition, List<Entities.EventOccurrence>>();
|
||||
public List<ParsingIssue> Issues { get; set; } = new();
|
||||
public List<string> SkippedHSSectionHeaders { get; set; } = new();
|
||||
public List<string> SkippedMSSectionHeaders { get; set; } = new();
|
||||
public int SkippedMSEventCount { get; set; }
|
||||
public int SkippedHSEventCount { get; set; }
|
||||
public List<string> SkippedSectionHeaders { get; set; } = new();
|
||||
public int SkippedEventCount { get; set; }
|
||||
/// <summary>
|
||||
/// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes.
|
||||
/// Multiple footnotes are concatenated into a single string.
|
||||
@@ -329,12 +327,12 @@ public class EventOccurrenceParser
|
||||
// School level is set - filter based on it
|
||||
if (_schoolLevel.Value == SchoolLevel.MiddleSchool && sectionSchoolLevel.Value == SchoolLevel.HighSchool)
|
||||
{
|
||||
result.SkippedHSSectionHeaders.Add(normalizedLine);
|
||||
result.SkippedSectionHeaders.Add(normalizedLine);
|
||||
return true;
|
||||
}
|
||||
if (_schoolLevel.Value == SchoolLevel.HighSchool && sectionSchoolLevel.Value == SchoolLevel.MiddleSchool)
|
||||
{
|
||||
result.SkippedMSSectionHeaders.Add(normalizedLine);
|
||||
result.SkippedSectionHeaders.Add(normalizedLine);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -379,12 +377,12 @@ public class EventOccurrenceParser
|
||||
// School level is set - filter based on it
|
||||
if (_schoolLevel.Value == SchoolLevel.MiddleSchool && currentSectionLevel.Value == SchoolLevel.HighSchool)
|
||||
{
|
||||
result.SkippedHSEventCount++;
|
||||
result.SkippedEventCount++;
|
||||
return true;
|
||||
}
|
||||
if (_schoolLevel.Value == SchoolLevel.HighSchool && currentSectionLevel.Value == SchoolLevel.MiddleSchool)
|
||||
{
|
||||
result.SkippedMSEventCount++;
|
||||
result.SkippedEventCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -110,10 +110,8 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
|
||||
result.Issues.AddRange(parserResult.Issues);
|
||||
|
||||
// Copy skipped section headers from parser result
|
||||
result.SkippedHSSectionHeaders.AddRange(parserResult.SkippedHSSectionHeaders);
|
||||
result.SkippedMSSectionHeaders.AddRange(parserResult.SkippedMSSectionHeaders);
|
||||
result.SkippedMSEventCount = parserResult.SkippedMSEventCount;
|
||||
result.SkippedHSEventCount = parserResult.SkippedHSEventCount;
|
||||
result.SkippedSectionHeaders.AddRange(parserResult.SkippedSectionHeaders);
|
||||
result.SkippedEventCount = parserResult.SkippedEventCount;
|
||||
|
||||
// Copy footnotes from parser result
|
||||
foreach (var kvp in parserResult.Footnotes)
|
||||
@@ -121,14 +119,10 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService
|
||||
result.Footnotes[kvp.Key] = kvp.Value;
|
||||
}
|
||||
|
||||
// Add informational messages about skipped events
|
||||
if (parserResult.SkippedMSEventCount > 0)
|
||||
// Add informational message about skipped events
|
||||
if (parserResult.SkippedEventCount > 0)
|
||||
{
|
||||
result.Warnings.Add($"Skipped {parserResult.SkippedMSEventCount} Middle School (MS) event occurrence(s) 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");
|
||||
result.Warnings.Add($"Skipped {parserResult.SkippedEventCount} event occurrence(s) from other school level based on school level setting");
|
||||
}
|
||||
|
||||
// Validate locations and add warnings for problematic ones
|
||||
|
||||
@@ -9,6 +9,16 @@ namespace Tests.Parsers;
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
@@ -41,21 +51,19 @@ public class EventOccurrenceParser_Tests
|
||||
return true;
|
||||
|
||||
// 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 - 20; i--)
|
||||
{
|
||||
if (i < fileLines.Count)
|
||||
for (int i = currentLineIndex - 1; i >= 0 && i >= currentLineIndex - MaxLookbackLines; i--)
|
||||
{
|
||||
if (i >= fileLines.Count) continue;
|
||||
|
||||
var line = fileLines[i].Trim();
|
||||
if (IsHighSchoolEvent(line))
|
||||
return true;
|
||||
if (IsMiddleSchoolEvent(line))
|
||||
return false; // Found MS section, so this is fixable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -88,7 +96,7 @@ public class EventOccurrenceParser_Tests
|
||||
/// <summary>
|
||||
/// Gets sample lines for a list of issues.
|
||||
/// </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
|
||||
.Take(count)
|
||||
@@ -96,6 +104,115 @@ public class EventOccurrenceParser_Tests
|
||||
.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>
|
||||
/// Counts HS vs MS event sections in the file.
|
||||
@@ -117,7 +234,50 @@ public class EventOccurrenceParser_Tests
|
||||
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
|
||||
|
||||
[Test]
|
||||
public void ParseNationalsTest()
|
||||
{
|
||||
@@ -130,53 +290,19 @@ public class EventOccurrenceParser_Tests
|
||||
{
|
||||
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;
|
||||
}
|
||||
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");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
WriteSpecialEvents(dictionary);
|
||||
|
||||
Assert.Pass();
|
||||
}
|
||||
@@ -194,53 +320,19 @@ public class EventOccurrenceParser_Tests
|
||||
{
|
||||
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;
|
||||
}
|
||||
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");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
WriteSpecialEvents(dictionary);
|
||||
|
||||
Assert.Pass();
|
||||
}
|
||||
@@ -259,83 +351,12 @@ public class EventOccurrenceParser_Tests
|
||||
// Assert - Should parse without exceptions
|
||||
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 totalLines = fileLines.Count;
|
||||
var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
|
||||
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
|
||||
Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)");
|
||||
}
|
||||
@@ -354,83 +375,12 @@ public class EventOccurrenceParser_Tests
|
||||
// Assert - Should parse without exceptions
|
||||
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 totalLines = fileLines.Count;
|
||||
var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
|
||||
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
|
||||
Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)");
|
||||
}
|
||||
@@ -449,83 +399,12 @@ public class EventOccurrenceParser_Tests
|
||||
// Assert - Should parse without exceptions
|
||||
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 totalLines = fileLines.Count;
|
||||
var (_, fixableIssues) = CategorizeIssues(result.Issues, fileLines);
|
||||
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
|
||||
Assert.Pass($"Successfully parsed {totalParsed} occurrences with {result.Issues.Count} issues ({fixableIssues.Count} fixable)");
|
||||
}
|
||||
@@ -536,7 +415,7 @@ public class EventOccurrenceParser_Tests
|
||||
// Arrange
|
||||
// Extract lines 64-92 from the test file - contains MS and HS events with various formats
|
||||
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 tempFile = EventOccurrenceParserTestHelpers.CreateTempFile(sectionContent);
|
||||
@@ -568,14 +447,14 @@ public class EventOccurrenceParser_Tests
|
||||
// Total expected MS occurrences: 16
|
||||
|
||||
var msEventCount = 0;
|
||||
if (childrensStories != null && result.Occurrences.ContainsKey(childrensStories))
|
||||
msEventCount += result.Occurrences[childrensStories].Count;
|
||||
if (coding != null && result.Occurrences.ContainsKey(coding))
|
||||
msEventCount += result.Occurrences[coding].Count;
|
||||
if (communityServiceVideo != null && result.Occurrences.ContainsKey(communityServiceVideo))
|
||||
msEventCount += result.Occurrences[communityServiceVideo].Count;
|
||||
if (constructionChallenge != null && result.Occurrences.ContainsKey(constructionChallenge))
|
||||
msEventCount += result.Occurrences[constructionChallenge].Count;
|
||||
if (childrensStories != null && result.Occurrences.TryGetValue(childrensStories, out var csOccurrences))
|
||||
msEventCount += csOccurrences.Count;
|
||||
if (coding != null && result.Occurrences.TryGetValue(coding, out var codingOccurrences))
|
||||
msEventCount += codingOccurrences.Count;
|
||||
if (communityServiceVideo != null && result.Occurrences.TryGetValue(communityServiceVideo, out var csvOccurrences))
|
||||
msEventCount += csvOccurrences.Count;
|
||||
if (constructionChallenge != null && result.Occurrences.TryGetValue(constructionChallenge, out var ccOccurrences))
|
||||
msEventCount += ccOccurrences.Count;
|
||||
|
||||
// When no school level is set, HS events should be processed (not skipped)
|
||||
// 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)
|
||||
).ToList();
|
||||
|
||||
// Verify HS section headers are NOT tracked in SkippedHSSectionHeaders when no school level is set
|
||||
var skippedHSHeaders = result.SkippedHSSectionHeaders;
|
||||
// Verify HS section headers are NOT tracked in SkippedSectionHeaders when no school level is set
|
||||
var skippedHeaders = result.SkippedSectionHeaders;
|
||||
|
||||
// Verify continuation lines are 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($"Total issues: {result.Issues.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($"\n--- Issue Types ---");
|
||||
@@ -625,7 +504,7 @@ public class EventOccurrenceParser_Tests
|
||||
// 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 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
|
||||
Assert.That(continuationLineIssues, Has.Count.EqualTo(0),
|
||||
"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");
|
||||
|
||||
// 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)
|
||||
.Where(loc => !string.IsNullOrWhiteSpace(loc))
|
||||
.ToList();
|
||||
@@ -680,13 +559,12 @@ public class EventOccurrenceParser_Tests
|
||||
|
||||
// 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)
|
||||
Assert.That(result.SkippedHSSectionHeaders, Does.Not.Contain("Biotechnology Design - HS"),
|
||||
"HS section header should NOT be in SkippedHSSectionHeaders when no school level is set");
|
||||
Assert.That(result.SkippedSectionHeaders, Does.Not.Contain("Biotechnology Design - HS"),
|
||||
"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
|
||||
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)
|
||||
// Expected: 2 MS occurrences (Submit Entry, Judging) + 3 HS occurrences (Submit Entry, Judging, Pick-up) = 5 total
|
||||
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"
|
||||
@attribute [Authorize]
|
||||
@using Core.Entities
|
||||
@using Core.Models
|
||||
@using Core.Services
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using MudBlazor
|
||||
@inject IEventOccurrenceParserService ParserService
|
||||
@inject AppDbContext Context
|
||||
@inject NavigationManager NavigationManager
|
||||
@@ -125,24 +122,22 @@
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Dense="true">
|
||||
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>
|
||||
}
|
||||
|
||||
@* Skipped Section Headers *@
|
||||
@if ((_parseResult.SkippedMSSectionHeaders.Any() || _parseResult.SkippedHSSectionHeaders.Any()) && _parseResult.IsSuccess)
|
||||
@if (_parseResult.SkippedSectionHeaders.Any() && _parseResult.IsSuccess)
|
||||
{
|
||||
<MudExpansionPanels Elevation="0" Class="mt-2">
|
||||
@if (_parseResult.SkippedMSSectionHeaders.Any())
|
||||
{
|
||||
<MudExpansionPanel Text="@($"Skipped MS Section Headers ({_parseResult.SkippedMSSectionHeaders.Count})")"
|
||||
<MudExpansionPanel Text="@($"Skipped Section Headers ({_parseResult.SkippedSectionHeaders.Count})")"
|
||||
Icon="@Icons.Material.Filled.Info"
|
||||
iconcolor="Color.Info">
|
||||
<MudList T="string">
|
||||
@foreach (var header in _parseResult.SkippedMSSectionHeaders)
|
||||
@foreach (var header in _parseResult.SkippedSectionHeaders)
|
||||
{
|
||||
<MudListItem T="string">
|
||||
<MudText>@header</MudText>
|
||||
@@ -150,22 +145,6 @@
|
||||
}
|
||||
</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>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
@page "/calendar"
|
||||
@attribute [Authorize]
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using WebApp.Models
|
||||
@using WebApp.Services
|
||||
@using Heron.MudCalendar
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using WebApp.Authentication
|
||||
@inject IEventOccurrenceService EventOccurrenceService
|
||||
@inject ILogger<Index> Logger
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageHeader Title="Event Calendar" Description="View competition schedules and event occurrences" Icon="@AppIcons.EventCalendar">
|
||||
<ActionButtons>
|
||||
@@ -25,17 +26,53 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="2">
|
||||
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small">
|
||||
<MudButton Color="@(_currentView == CalendarView.Month ? Color.Primary : Color.Default)"
|
||||
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="CalendarView.Day"
|
||||
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>
|
||||
|
||||
@code {
|
||||
private List<CalendarEventItem>? _calendarItems;
|
||||
private DateTime _calendarDate = DateTime.Today;
|
||||
private CalendarView _currentView = CalendarView.Month;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@@ -49,32 +86,33 @@
|
||||
Logger.LogInformation("Loading calendar events");
|
||||
var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync();
|
||||
|
||||
if (occurrences == null)
|
||||
{
|
||||
Logger.LogWarning("Service returned null occurrences");
|
||||
_calendarItems = new List<CalendarEventItem>();
|
||||
return;
|
||||
}
|
||||
var eventOccurrences = occurrences as EventOccurrence[] ?? occurrences.ToArray();
|
||||
Logger.LogDebug("Received {Count} occurrences from service", eventOccurrences.Count());
|
||||
|
||||
Logger.LogDebug("Received {Count} occurrences from service", occurrences.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>();
|
||||
foreach (var occ in occurrences)
|
||||
foreach (var occ in eventOccurrences)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (occ == null)
|
||||
{
|
||||
Logger.LogWarning("Null occurrence found, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(occ.Name))
|
||||
{
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -87,7 +125,7 @@
|
||||
|
||||
_calendarItems = items;
|
||||
Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences",
|
||||
_calendarItems.Count, occurrences.Count());
|
||||
_calendarItems.Count, eventOccurrences.Count());
|
||||
|
||||
// Find the next date with events
|
||||
_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()
|
||||
{
|
||||
try
|
||||
@@ -119,7 +186,7 @@
|
||||
{
|
||||
try
|
||||
{
|
||||
return item != null && item.Start.Date >= today;
|
||||
return item.Start.Date >= today;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -137,16 +204,10 @@
|
||||
|
||||
// Fallback to first event if no future events
|
||||
var firstEvent = _calendarItems
|
||||
.Where(item => item != null)
|
||||
.OrderBy(item => item.Start)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (firstEvent != null)
|
||||
{
|
||||
return firstEvent.Start.Date;
|
||||
}
|
||||
|
||||
return DateTime.Today;
|
||||
return firstEvent != null ? firstEvent.Start.Date : DateTime.Today;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -154,5 +215,67 @@
|
||||
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 Property="@(e => e.Grade)" Title="Grade (TSA Year)" Sortable="true">
|
||||
<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>
|
||||
</PropertyColumn>
|
||||
</Columns>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using Core.Entities;
|
||||
using Heron.MudCalendar;
|
||||
using MudBlazor;
|
||||
|
||||
namespace WebApp.Models;
|
||||
|
||||
@@ -12,12 +12,17 @@ public class CalendarEventItem : CalendarItem
|
||||
/// <summary>
|
||||
/// Gets the original EventOccurrence data.
|
||||
/// </summary>
|
||||
public Core.Entities.EventOccurrence? EventOccurrenceData { get; set; }
|
||||
public EventOccurrence? EventOccurrenceData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the associated EventDefinition if available.
|
||||
/// </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>
|
||||
/// Parameterless constructor required by Heron.MudCalendar component.
|
||||
@@ -30,30 +35,15 @@ public class CalendarEventItem : CalendarItem
|
||||
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
|
||||
Text = GetEventTitle(occurrence, eventDefinition);
|
||||
StudentFirstNames = studentFirstNames?.ToList() ?? [];
|
||||
Text = occurrence.EventDefinition?.ShortName;
|
||||
Start = occurrence.StartTime;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,24 @@ public class EventOccurrenceService : IEventOccurrenceService
|
||||
.OrderBy(eo => eo.StartTime)
|
||||
.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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user