diff --git a/Core/Models/EventOccurrenceParseResult.cs b/Core/Models/EventOccurrenceParseResult.cs index f7f4edd..a83b758 100644 --- a/Core/Models/EventOccurrenceParseResult.cs +++ b/Core/Models/EventOccurrenceParseResult.cs @@ -31,26 +31,17 @@ public class EventOccurrenceParseResult public List Issues { get; set; } = new(); /// - /// 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. /// - public List SkippedHSSectionHeaders { get; set; } = new(); + public List SkippedSectionHeaders { get; set; } = new(); /// - /// 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. /// - public List SkippedMSSectionHeaders { get; set; } = new(); - - /// - /// Number of Middle School (MS) event occurrences that were skipped due to school level filtering. - /// - public int SkippedMSEventCount { get; set; } - - /// - /// Number of High School (HS) event occurrences that were skipped due to school level filtering. - /// - public int SkippedHSEventCount { get; set; } + public int SkippedEventCount { get; set; } /// /// Footnotes captured for each event definition. Footnotes are lines that start with "*" or are parenthetical notes. diff --git a/Core/Parsers/EventOccurrenceParser.cs b/Core/Parsers/EventOccurrenceParser.cs index e256f99..6184c5a 100644 --- a/Core/Parsers/EventOccurrenceParser.cs +++ b/Core/Parsers/EventOccurrenceParser.cs @@ -14,10 +14,8 @@ public class EventOccurrenceParserResult { public IDictionary> Occurrences { get; set; } = new Dictionary>(); public List Issues { get; set; } = new(); - public List SkippedHSSectionHeaders { get; set; } = new(); - public List SkippedMSSectionHeaders { get; set; } = new(); - public int SkippedMSEventCount { get; set; } - public int SkippedHSEventCount { get; set; } + public List SkippedSectionHeaders { get; set; } = new(); + public int SkippedEventCount { get; set; } /// /// 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; } diff --git a/Core/Services/EventOccurrenceParserService.cs b/Core/Services/EventOccurrenceParserService.cs index ced5090..7e070e8 100644 --- a/Core/Services/EventOccurrenceParserService.cs +++ b/Core/Services/EventOccurrenceParserService.cs @@ -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 diff --git a/Tests/Parsers/EventOccurrenceParser_Tests.cs b/Tests/Parsers/EventOccurrenceParser_Tests.cs index bc084e6..a524418 100644 --- a/Tests/Parsers/EventOccurrenceParser_Tests.cs +++ b/Tests/Parsers/EventOccurrenceParser_Tests.cs @@ -9,6 +9,16 @@ namespace Tests.Parsers; /// 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 /// @@ -41,20 +51,18 @@ 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 - MaxLookbackLines; i--) { - // Look backwards to find the most recent section header - for (int i = currentLineIndex - 1; i >= 0 && i >= currentLineIndex - 20; i--) - { - if (i < fileLines.Count) - { - var line = fileLines[i].Trim(); - if (IsHighSchoolEvent(line)) - return true; - if (IsMiddleSchoolEvent(line)) - return false; // Found MS section, so this is fixable - } - } + 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 /// /// Gets sample lines for a list of issues. /// - private static List GetSampleLines(List issues, int count = 5) + private static List GetSampleLines(List issues, int count = SampleIssuesCount) { return issues .Take(count) @@ -96,6 +104,115 @@ public class EventOccurrenceParser_Tests .ToList(); } + /// + /// Writes special events summary to console. + /// + private static void WriteSpecialEventsSummary(IDictionary> 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"); + } + + /// + /// Writes issue breakdown by type to console. + /// + private static void WriteIssueBreakdown( + Dictionary> issuesByType, + Dictionary expectedByType, + Dictionary fixableByType, + List 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); + } + } + } + } + + /// + /// Writes unmatched line analysis to console. + /// + private static void WriteUnmatchedLineAnalysis(List 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)"); + } + } + + /// + /// Analyzes and reports parsing results for a file. + /// + 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); + } + /// /// Counts HS vs MS event sections in the file. @@ -117,68 +234,77 @@ public class EventOccurrenceParser_Tests return (hsCount, msCount); } + /// + /// Writes special events to console output. + /// + private static void WriteSpecialEvents(IDictionary> 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() { - var events = TestEntityHandler.GetEvents(); - var parser = new EventOccurrenceParser(TestEntityHandler.GetEventOccurrenceNationalsFileInfo(), events); - var result = parser.Parse(); - var dictionary = result.Occurrences; - Console.WriteLine($"Occurrence, Month, Date, Time, Location"); - foreach (var @event in events) - { - Console.WriteLine($"{@event.Name}"); + var events = TestEntityHandler.GetEvents(); + var parser = new EventOccurrenceParser(TestEntityHandler.GetEventOccurrenceNationalsFileInfo(), events); + var result = parser.Parse(); + var dictionary = result.Occurrences; + Console.WriteLine($"Occurrence, Month, Date, Time, Location"); + foreach (var @event in events) + { + Console.WriteLine($"{@event.Name}"); - if (!dictionary.ContainsKey(@event)) - { - 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}"); - } - } + if (!dictionary.TryGetValue(@event, out var eventOccurrences)) + { + Console.WriteLine($"!!! eventDefinition not found {@event.Name}"); + continue; + } - 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}"); - } - } + foreach (var eo in eventOccurrences) + { + 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}"); - } - } + WriteSpecialEvents(dictionary); - 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(); } @@ -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,82 +351,11 @@ 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,82 +375,11 @@ 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,82 +399,11 @@ 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), diff --git a/WebApp/Components/Features/Calendar/EventOccurrenceDetailsDialog.razor b/WebApp/Components/Features/Calendar/EventOccurrenceDetailsDialog.razor new file mode 100644 index 0000000..74bdd01 --- /dev/null +++ b/WebApp/Components/Features/Calendar/EventOccurrenceDetailsDialog.razor @@ -0,0 +1,79 @@ +@using Core.Entities +@using MudBlazor + + + + Event Details + + + @if (EventOccurrence == null) + { + No event data available. + } + else + { + + @if (EventDefinition != null) + { +
+ Event + @EventDefinition.Name +
+ } + +
+ Occurrence + @EventOccurrence.Name +
+ +
+ Date & Time + + @EventOccurrence.StartTime.ToString("f") + @if (EventOccurrence.EndTime.HasValue) + { + - @EventOccurrence.EndTime.Value.ToString("t") + } + +
+ + @if (!string.IsNullOrEmpty(EventOccurrence.Location)) + { +
+ Location + + + @EventOccurrence.Location + +
+ } + + @if (!string.IsNullOrEmpty(EventOccurrence.Time)) + { +
+ Time + @EventOccurrence.Time +
+ } + + @if (StudentFirstNames != null && StudentFirstNames.Any()) + { +
+ Students + + + @string.Join(", ", StudentFirstNames) + +
+ } +
+ } +
+
+ +@code { + [Parameter] public EventOccurrence? EventOccurrence { get; set; } + [Parameter] public EventDefinition? EventDefinition { get; set; } + [Parameter] public List? StudentFirstNames { get; set; } +} + diff --git a/WebApp/Components/Features/Calendar/Import.razor b/WebApp/Components/Features/Calendar/Import.razor index d69f037..569b508 100644 --- a/WebApp/Components/Features/Calendar/Import.razor +++ b/WebApp/Components/Features/Calendar/Import.razor @@ -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,47 +122,29 @@ { Successfully parsed @_parseResult.TotalParsed occurrence(s) from @_parseResult.Occurrences.Count event definition(s) - @if (_parseResult.SkippedMSEventCount > 0 || _parseResult.SkippedHSEventCount > 0) + @if (_parseResult.SkippedEventCount > 0) { - (Skipped @(_parseResult.SkippedMSEventCount + _parseResult.SkippedHSEventCount) event occurrence(s) from other school level) + (Skipped @_parseResult.SkippedEventCount event occurrence(s) from other school level) } } @* Skipped Section Headers *@ - @if ((_parseResult.SkippedMSSectionHeaders.Any() || _parseResult.SkippedHSSectionHeaders.Any()) && _parseResult.IsSuccess) + @if (_parseResult.SkippedSectionHeaders.Any() && _parseResult.IsSuccess) { - @if (_parseResult.SkippedMSSectionHeaders.Any()) - { - - - @foreach (var header in _parseResult.SkippedMSSectionHeaders) - { - - @header - - } - - - } - @if (_parseResult.SkippedHSSectionHeaders.Any()) - { - - - @foreach (var header in _parseResult.SkippedHSSectionHeaders) - { - - @header - - } - - - } + + + @foreach (var header in _parseResult.SkippedSectionHeaders) + { + + @header + + } + + } diff --git a/WebApp/Components/Features/Calendar/Index.razor b/WebApp/Components/Features/Calendar/Index.razor index fc7b4fe..32882d6 100644 --- a/WebApp/Components/Features/Calendar/Index.razor +++ b/WebApp/Components/Features/Calendar/Index.razor @@ -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 Logger +@inject IDialogService DialogService @@ -25,17 +26,53 @@ } else { - + + + + Month + + + Day + + + + + + @* *@ +
+ +
@context.Text
+
+ @*
*@ +
+ + @* *@ +
+ @context.Text +
+ @*
*@ +
+ + @* *@ +
@context.Text
+ @*
*@ +
+
+
} @code { private List? _calendarItems; private DateTime _calendarDate = DateTime.Today; + private CalendarView _currentView = CalendarView.Month; protected override async Task OnInitializedAsync() { @@ -48,33 +85,34 @@ { Logger.LogInformation("Loading calendar events"); var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync(); - - if (occurrences == null) - { - Logger.LogWarning("Service returned null occurrences"); - _calendarItems = new List(); - 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(); - 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 StudentFirstNames(EventDefinition ed, Dictionary> teamsByEventId) + { + var studentFirstNames = new List(); + 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(); + + 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("Event Details", parameters, options); + } } diff --git a/WebApp/Components/Features/Students/Index.razor b/WebApp/Components/Features/Students/Index.razor index 3abf087..47fbb65 100644 --- a/WebApp/Components/Features/Students/Index.razor +++ b/WebApp/Components/Features/Students/Index.razor @@ -55,7 +55,7 @@ - @((MarkupString)AppIcons.GetOrdinalSuperscript(context.Item.Grade)) (@context.Item.TsaYear) + @((MarkupString)AppIcons.GetOrdinalSuperscript(context.Item.Grade)) (@context.Item.TsaYear) diff --git a/WebApp/Models/CalendarEventItem.cs b/WebApp/Models/CalendarEventItem.cs index b6cab82..0fe6733 100644 --- a/WebApp/Models/CalendarEventItem.cs +++ b/WebApp/Models/CalendarEventItem.cs @@ -1,5 +1,5 @@ +using Core.Entities; using Heron.MudCalendar; -using MudBlazor; namespace WebApp.Models; @@ -12,12 +12,17 @@ public class CalendarEventItem : CalendarItem /// /// Gets the original EventOccurrence data. /// - public Core.Entities.EventOccurrence? EventOccurrenceData { get; set; } + public EventOccurrence? EventOccurrenceData { get; set; } /// /// Gets the associated EventDefinition if available. /// - public Core.Entities.EventDefinition? EventDefinition { get; set; } + public EventDefinition? EventDefinition { get; set; } + + /// + /// Gets the list of student first names from teams matching the EventDefinition. + /// + public List StudentFirstNames { get; set; } = []; /// /// 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? 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; } } diff --git a/WebApp/Services/EventOccurrenceService.cs b/WebApp/Services/EventOccurrenceService.cs index cdbf9ba..4b68798 100644 --- a/WebApp/Services/EventOccurrenceService.cs +++ b/WebApp/Services/EventOccurrenceService.cs @@ -29,5 +29,24 @@ public class EventOccurrenceService : IEventOccurrenceService .OrderBy(eo => eo.StartTime) .ToListAsync(); } + + public async Task>> GetTeamsByEventDefinitionIdsAsync(IEnumerable eventDefinitionIds) + { + var ids = eventDefinitionIds.Where(id => id > 0).Distinct().ToList(); + if (!ids.Any()) + { + return new Dictionary>(); + } + + 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()); + } } diff --git a/WebApp/Services/IEventOccurrenceService.cs b/WebApp/Services/IEventOccurrenceService.cs index 47a8f31..70e5665 100644 --- a/WebApp/Services/IEventOccurrenceService.cs +++ b/WebApp/Services/IEventOccurrenceService.cs @@ -13,5 +13,10 @@ public interface IEventOccurrenceService /// Gets event occurrences within the specified date range. /// Task> GetEventOccurrencesForDateRangeAsync(DateTime start, DateTime end); + + /// + /// Gets all teams for the specified event definition IDs, including students. + /// + Task>> GetTeamsByEventDefinitionIdsAsync(IEnumerable eventDefinitionIds); }