diff --git a/Core/Models/LocationParsingConfiguration.cs b/Core/Models/LocationParsingConfiguration.cs deleted file mode 100644 index 44e6bf1..0000000 --- a/Core/Models/LocationParsingConfiguration.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Core.Models; - -/// -/// Configuration for location parsing patterns used in event occurrence parsing. -/// Supports venue-specific room naming conventions. -/// -public class LocationParsingConfiguration -{ - /// - /// List of location prefix patterns (e.g., ["Room *", "Hall *", "Conference Room *"]). - /// Patterns use "*" as wildcard to match any text after the prefix. - /// - public List LocationPatterns { get; set; } = new(); - - /// - /// Default location parsing configuration with common patterns. - /// - public static LocationParsingConfiguration Default => new() - { - LocationPatterns = new List - { - "Room *", - "Hall *", - "Exhibit Hall *", - "Conference Room *", - "Building *", - "Auditorium *", - "Mtg. Room *", - "Meeting Room *", - "Banquet Room *", - "Banquet Hall *", - "Online", - "Virtual", - "TBD" - } - }; -} - - diff --git a/Core/Parsers/EventOccurrence/LocationPatternMatcher.cs b/Core/Parsers/EventOccurrence/LocationPatternMatcher.cs deleted file mode 100644 index 984b7f9..0000000 --- a/Core/Parsers/EventOccurrence/LocationPatternMatcher.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Core.Parsers.EventOccurrence; - -/// -/// Matches location strings against configurable patterns. -/// Supports exact matches and wildcard patterns (e.g., "Room *", "Exhibit Hall *"). -/// -public static class LocationPatternMatcher -{ - /// - /// Matches location text against configured patterns and returns the matched location. - /// - /// The location text to match. - /// The list of patterns to match against. - /// The matched location if a pattern matches, or empty string if no match is found. - public static string Match(string locationText, IReadOnlyList patterns) - { - // Normalize location text for matching (trim and handle variations) - var normalizedLocation = locationText.Trim(); - - // If location is empty after normalization, return empty - if (string.IsNullOrWhiteSpace(normalizedLocation)) - return string.Empty; - - foreach (var pattern in patterns) - { - var normalizedPattern = pattern.Trim(); - - // Skip empty patterns - if (string.IsNullOrWhiteSpace(normalizedPattern)) - continue; - - // Handle exact matches (patterns without wildcards like "Online", "Virtual", "TBD") - if (!normalizedPattern.Contains('*')) - { - if (string.Equals(normalizedPattern, normalizedLocation, StringComparison.OrdinalIgnoreCase)) - { - return normalizedLocation; - } - continue; - } - - // Convert pattern to regex: escape special chars, replace * with .* - // This handles patterns like "Exhibit Hall *", "Room *", "Mtg. Room *", etc. - var escapedPattern = Regex.Escape(normalizedPattern); - escapedPattern = escapedPattern.Replace(@"\*", ".*?"); - - // Use case-insensitive matching - // Note: For dynamic patterns, we compile on demand. This is acceptable since patterns - // are configured and reused across parsing sessions. - var regex = new Regex($"^{escapedPattern}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); - if (regex.IsMatch(normalizedLocation)) - { - return normalizedLocation; // Return the full matched location - } - } - - return string.Empty; - } -} - diff --git a/Core/Services/EventOccurrenceParserService.cs b/Core/Services/EventOccurrenceParserService.cs index 97c6283..1b15ebf 100644 --- a/Core/Services/EventOccurrenceParserService.cs +++ b/Core/Services/EventOccurrenceParserService.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.RegularExpressions; using Core.Entities; using Core.Models; using Core.Parsers; @@ -93,6 +94,9 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService // Copy skipped HS section headers from parser result result.SkippedHSSectionHeaders.AddRange(parserResult.SkippedHSSectionHeaders); + + // Validate locations and add warnings for problematic ones + ValidateLocations(result); } finally { @@ -121,5 +125,48 @@ public class EventOccurrenceParserService : IEventOccurrenceParserService return result; } + + /// + /// Validates locations from parsed occurrences and adds warnings for problematic locations. + /// + private static void ValidateLocations(EventOccurrenceParseResult result) + { + // Collect all unique locations + var locations = result.Occurrences.Values + .SelectMany(list => list) + .Select(eo => eo.Location) + .Where(loc => !string.IsNullOrWhiteSpace(loc)) + .Distinct() + .ToList(); + + if (!locations.Any()) + return; + + // Check for long locations (>50 chars) + var longLocations = locations.Where(loc => loc != null && loc.Length > 50).ToList(); + foreach (var loc in longLocations) + { + if (loc != null) + { + result.Warnings.Add($"Location '{loc}' is unusually long ({loc.Length} characters) and may contain multiple lines or extra text"); + } + } + + // Check for date/time patterns + // Pattern matches: month names with day numbers, time patterns (HH:MM AM/PM), and NOON + var dateTimePattern = new Regex( + @"\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}\b|\b\d{1,2}:\d{2}\s*(a|p)\.?m\.?\b|\bNOON\b", + RegexOptions.IgnoreCase | RegexOptions.Compiled); + + var locationsWithDateTime = locations.Where(loc => loc != null && dateTimePattern.IsMatch(loc)).ToList(); + foreach (var loc in locationsWithDateTime) + { + if (loc != null) + { + var match = dateTimePattern.Match(loc); + result.Warnings.Add($"Location '{loc}' may contain date/time information: '{match.Value}'"); + } + } + } } diff --git a/Tests/Parsers/EventOccurrence/LocationPatternMatcher_Tests.cs b/Tests/Parsers/EventOccurrence/LocationPatternMatcher_Tests.cs deleted file mode 100644 index 432bc4c..0000000 --- a/Tests/Parsers/EventOccurrence/LocationPatternMatcher_Tests.cs +++ /dev/null @@ -1,166 +0,0 @@ -using Core.Parsers.EventOccurrence; -using NUnit.Framework; - -namespace Tests.Parsers.EventOccurrence; - -[TestFixture] -public class LocationPatternMatcher_Tests -{ - [Test] - public void Match_ExactMatch_ReturnsLocation() - { - // Arrange - var patterns = new List { "Room A", "Room B", "Hall C" }; - - // Act - var result = LocationPatternMatcher.Match("Room A", patterns); - - // Assert - Assert.That(result, Is.EqualTo("Room A")); - } - - [Test] - public void Match_ExactMatch_CaseInsensitive_ReturnsLocation() - { - // Arrange - var patterns = new List { "Room A" }; - - // Act - var result = LocationPatternMatcher.Match("room a", patterns); - - // Assert - Assert.That(result, Is.EqualTo("room a")); - } - - [Test] - public void Match_WildcardPattern_Matches_ReturnsLocation() - { - // Arrange - var patterns = new List { "Room *", "Hall *" }; - - // Act - var result = LocationPatternMatcher.Match("Room 101", patterns); - - // Assert - Assert.That(result, Is.EqualTo("Room 101")); - } - - [Test] - public void Match_WildcardPattern_MultipleMatches_ReturnsFirstMatch() - { - // Arrange - var patterns = new List { "Room *", "Exhibit Hall *" }; - - // Act - var result = LocationPatternMatcher.Match("Room 202", patterns); - - // Assert - Assert.That(result, Is.EqualTo("Room 202")); - } - - [Test] - public void Match_WildcardPattern_ExhibitHall_Matches() - { - // Arrange - var patterns = new List { "Exhibit Hall *" }; - - // Act - var result = LocationPatternMatcher.Match("Exhibit Hall C", patterns); - - // Assert - Assert.That(result, Is.EqualTo("Exhibit Hall C")); - } - - [Test] - public void Match_WildcardPattern_MtgRoom_Matches() - { - // Arrange - var patterns = new List { "Mtg. Room *" }; - - // Act - var result = LocationPatternMatcher.Match("Mtg. Room 14", patterns); - - // Assert - Assert.That(result, Is.EqualTo("Mtg. Room 14")); - } - - [Test] - public void Match_NoMatch_ReturnsEmpty() - { - // Arrange - var patterns = new List { "Room *", "Hall *" }; - - // Act - var result = LocationPatternMatcher.Match("Unknown Location", patterns); - - // Assert - Assert.That(result, Is.Empty); - } - - [Test] - public void Match_EmptyLocation_ReturnsEmpty() - { - // Arrange - var patterns = new List { "Room *" }; - - // Act - var result = LocationPatternMatcher.Match("", patterns); - - // Assert - Assert.That(result, Is.Empty); - } - - [Test] - public void Match_WhitespaceLocation_ReturnsEmpty() - { - // Arrange - var patterns = new List { "Room *" }; - - // Act - var result = LocationPatternMatcher.Match(" ", patterns); - - // Assert - Assert.That(result, Is.Empty); - } - - [Test] - public void Match_EmptyPatterns_ReturnsEmpty() - { - // Arrange - var patterns = new List(); - - // Act - var result = LocationPatternMatcher.Match("Room A", patterns); - - // Assert - Assert.That(result, Is.Empty); - } - - [Test] - public void Match_PatternWithSpecialCharacters_EscapesCorrectly() - { - // Arrange - var patterns = new List { "Room (A)" }; - - // Act - var result = LocationPatternMatcher.Match("Room (A)", patterns); - - // Assert - Assert.That(result, Is.EqualTo("Room (A)")); - } - - [Test] - public void Match_LocationWithWhitespace_Trims_ReturnsLocation() - { - // Arrange - var patterns = new List { "Room *" }; - - // Act - var result = LocationPatternMatcher.Match(" Room 101 ", patterns); - - // Assert - // LocationPatternMatcher returns the matched location after normalization (trim) - Assert.That(result, Is.EqualTo("Room 101")); - } -} - diff --git a/Tests/Parsers/EventOccurrenceParserTestHelpers.cs b/Tests/Parsers/EventOccurrenceParserTestHelpers.cs index c4b380f..cc2a965 100644 --- a/Tests/Parsers/EventOccurrenceParserTestHelpers.cs +++ b/Tests/Parsers/EventOccurrenceParserTestHelpers.cs @@ -1,5 +1,4 @@ using Core.Entities; -using Core.Models; using Tests.Builders; namespace Tests.Parsers; @@ -27,17 +26,6 @@ public static class EventOccurrenceParserTestHelpers return EventDefinitionBuilder.Individual(name).Build(); } - /// - /// Creates a LocationParsingConfiguration for testing. - /// - public static LocationParsingConfiguration CreateLocationConfig(params string[] patterns) - { - return new LocationParsingConfiguration - { - LocationPatterns = patterns.ToList() - }; - } - /// /// Cleans up a temporary file. /// diff --git a/WebApp/Components/Features/Calendar/Import.razor b/WebApp/Components/Features/Calendar/Import.razor index 1f7ea3a..3a3ce07 100644 --- a/WebApp/Components/Features/Calendar/Import.razor +++ b/WebApp/Components/Features/Calendar/Import.razor @@ -21,17 +21,7 @@ - - Paste Event Occurrence Data - - Location Settings - - + Paste Event Occurrence Data @@ -138,6 +128,53 @@ } + @* Locations Summary *@ + @if (_parseResult.IsSuccess && _parseResult.Occurrences.Any()) + { + var allLocations = _parseResult.Occurrences.Values + .SelectMany(list => list) + .Select(eo => eo.Location) + .Where(loc => !string.IsNullOrWhiteSpace(loc)) + .Distinct() + .OrderBy(loc => loc) + .ToList(); + + // Check which locations have warnings (long or contain date/time) + var dateTimePattern = new System.Text.RegularExpressions.Regex( + @"\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+\d{1,2}\b|\b\d{1,2}:\d{2}\s*(a|p)\.?m\.?\b|\bNOON\b", + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + var longLocations = allLocations.Where(loc => loc.Length > 50).ToList(); + var locationsWithDateTime = allLocations.Where(loc => dateTimePattern.IsMatch(loc)).ToList(); + + @if (allLocations.Any()) + { + var warningCount = longLocations.Count + locationsWithDateTime.Count; + Parsed Locations (@allLocations.Count unique@(warningCount > 0 ? $", {warningCount} with warnings" : "")) + + + + @foreach (var location in allLocations) + { + var isLong = longLocations.Contains(location); + var hasDateTime = locationsWithDateTime.Contains(location); + var hasWarning = isLong || hasDateTime; + + + @if (hasWarning) + { + Warning + } + @location + + + } + + + + } + } + @* Parsed Occurrences List *@ @if (_parseResult.IsSuccess && _parseResult.Occurrences.Any()) { @@ -307,9 +344,5 @@ }; } - private void OpenLocationSettings() - { - NavigationManager.NavigateTo("/calendar/event-occurrences/import/settings"); - } } diff --git a/WebApp/Components/Features/Calendar/LocationParsingSettings.razor b/WebApp/Components/Features/Calendar/LocationParsingSettings.razor deleted file mode 100644 index bb9705b..0000000 --- a/WebApp/Components/Features/Calendar/LocationParsingSettings.razor +++ /dev/null @@ -1,233 +0,0 @@ -@page "/calendar/event-occurrences/import/settings" -@attribute [Authorize(Roles = AuthRoles.Administrator)] -@using Core.Models -@using WebApp.Authentication -@using WebApp.Components.Shared.Components -@using System.Text.Json -@using MudBlazor -@inject IWebHostEnvironment Environment -@inject IConfiguration Configuration -@inject NavigationManager NavigationManager -@inject ISnackbar Snackbar - -@rendermode InteractiveServer - - - - - @if (_config != null) - { - - Location Patterns - - Add prefix patterns to match location names. Use '*' as a wildcard to match any text after the prefix. - Examples: "Room *" matches "Room 101", "Room 202"; "Hall *" matches "Hall A", "Main Hall". - - - - @for (int i = 0; i < _config.LocationPatterns.Count; i++) - { - var index = i; - - - - Remove - - - } - - - Add Pattern - - - - - - Pattern Examples - - - - Pattern - Matches - - - - - Room * - Room 101, Room 202, Room A - - - Hall * - Hall A, Hall B, Main Hall - - - Conference Room * - Conference Room A, Conference Room 1 - - - Building * - Building 1, Building A, Building Main - - - - - - - - - - @if (_isSaving) - { - - Saving... - } - else - { - Save Configuration - } - - - Cancel - - - - - - @if (!string.IsNullOrEmpty(_statusMessage)) - { - @_statusMessage - } - } - else - { - - } - - -@code { - private LocationParsingConfiguration? _config; - private bool _isSaving; - private string? _statusMessage; - private Severity _statusSeverity = Severity.Success; - - protected override void OnInitialized() - { - // Load from IConfiguration - _config = Configuration.GetSection("LocationParsingSettings").Get() - ?? LocationParsingConfiguration.Default; - - // Create a copy to avoid modifying the original - _config = new LocationParsingConfiguration - { - LocationPatterns = new List(_config.LocationPatterns) - }; - } - - private string GetAppSettingsPath() - { - return Path.Combine( - Environment.ContentRootPath, - "Data", - "appsettings.json"); - } - - private void AddPattern() - { - if (_config == null) return; - _config.LocationPatterns.Add("New Pattern *"); - } - - private void RemovePattern(int index) - { - if (_config == null || index < 0 || index >= _config.LocationPatterns.Count) return; - _config.LocationPatterns.RemoveAt(index); - } - - private async Task SaveConfiguration() - { - if (_config == null) return; - - _isSaving = true; - _statusMessage = null; - - try - { - var appSettingsPath = GetAppSettingsPath(); - - // Ensure Data directory exists - var dataDir = Path.GetDirectoryName(appSettingsPath); - if (dataDir != null && !Directory.Exists(dataDir)) - { - Directory.CreateDirectory(dataDir); - } - - // Read existing appsettings or create new - Dictionary settings; - - if (File.Exists(appSettingsPath)) - { - var existingJson = await File.ReadAllTextAsync(appSettingsPath); - settings = JsonSerializer.Deserialize>(existingJson) - ?? new Dictionary(); - } - else - { - settings = new Dictionary(); - } - - // Update LocationParsingSettings section - settings["LocationParsingSettings"] = _config; - - // Write back to file - var options = new JsonSerializerOptions { WriteIndented = true }; - var json = JsonSerializer.Serialize(settings, options); - await File.WriteAllTextAsync(appSettingsPath, json); - - _statusMessage = "Configuration saved successfully! Changes will take effect on next parse operation."; - _statusSeverity = Severity.Success; - Snackbar.Add("Location parsing settings saved successfully", Severity.Success); - } - catch (Exception ex) - { - _statusMessage = $"Error saving configuration: {ex.Message}"; - _statusSeverity = Severity.Error; - Snackbar.Add($"Error saving settings: {ex.Message}", Severity.Error); - } - finally - { - _isSaving = false; - } - } - - private void Cancel() - { - NavigationManager.NavigateTo("/calendar/event-occurrences/import"); - } -} - - diff --git a/WebApp/Program.cs b/WebApp/Program.cs index 9cd5f3d..932b867 100644 --- a/WebApp/Program.cs +++ b/WebApp/Program.cs @@ -45,11 +45,6 @@ if (!File.Exists(dataAppSettingsPath)) templateSettings["ValidationSettings"] = JsonSerializer.Deserialize(validationSettings.GetRawText()); } - if (baseDoc.RootElement.TryGetProperty("LocationParsingSettings", out var locationParsingSettings)) - { - templateSettings["LocationParsingSettings"] = JsonSerializer.Deserialize(locationParsingSettings.GetRawText()); - } - if (templateSettings.Any()) { var json = JsonSerializer.Serialize(templateSettings, new JsonSerializerOptions diff --git a/WebApp/appsettings.json b/WebApp/appsettings.json index e38689a..91d81b9 100644 --- a/WebApp/appsettings.json +++ b/WebApp/appsettings.json @@ -35,20 +35,5 @@ "EventCountSeverity": "Warning", "MissingCaptainSeverity": "Warning", "TooManyRegionalEventsSeverity": "Warning" - }, - "LocationParsingSettings": { - "LocationPatterns": [ - "Room *", - "Hall *", - "Conference Room *", - "Building *", - "Auditorium *", - "Exhibit Hall *", - "Mtg. Room *", - "Meeting Room *", - "Banquet Room *", - "Online", - "Virtual" - ] } }
Room *
Hall *
Conference Room *
Building *