This commit introduces a new SchoolLevel enum and updates the EventOccurrenceParser to filter event occurrences based on the specified school level (Middle School or High School). The EventOccurrenceParseResult and EventOccurrenceParserResult classes have been updated to track skipped section headers and counts for both school levels. Additionally, the EventOccurrenceParserService has been modified to read the school level from configuration, and the UI has been updated to allow users to select the school level for event imports. This enhancement improves the accuracy of event parsing and provides better user feedback on skipped occurrences.
390 lines
18 KiB
Plaintext
390 lines
18 KiB
Plaintext
@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
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
|
|
<PageHeader
|
|
Title="Import Event Occurrences"
|
|
Description="Parse and import event occurrence data from text"
|
|
ShowBackButton="true"
|
|
BackButtonUrl="/calendar/event-occurrences" />
|
|
|
|
<MudGrid>
|
|
<MudItem xs="12" md="6">
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
|
|
<MudText Typo="Typo.h5" Class="mb-4">Paste Event Occurrence Data</MudText>
|
|
<MudStack Spacing="3">
|
|
|
|
<MudStack Row="true" Spacing="2">
|
|
<MudButton
|
|
Variant="Variant.Filled"
|
|
Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.Article"
|
|
OnClick="HandleParse"
|
|
Disabled="@_isParsing">
|
|
Parse
|
|
</MudButton>
|
|
<MudButton
|
|
Variant="Variant.Text"
|
|
OnClick="HandleClear"
|
|
Disabled="@_isParsing">
|
|
Clear
|
|
</MudButton>
|
|
</MudStack>
|
|
<MudTextField
|
|
T="string"
|
|
Label="Event Occurrence Text"
|
|
@bind-Value="_inputText"
|
|
|
|
Variant="Variant.Outlined"
|
|
Lines="15"
|
|
AutoGrow="true"
|
|
Placeholder="Paste event occurrence text here..."
|
|
HelperText="Paste the event schedule text in the format expected by the parser" />
|
|
<!-- @bind-Value:event="oninput" -->
|
|
</MudStack>
|
|
</MudPaper>
|
|
</MudItem>
|
|
|
|
<MudItem xs="12" md="6">
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
|
|
<MudText Typo="Typo.h5" Class="mb-4">Parsed Results</MudText>
|
|
|
|
@if (_isParsing)
|
|
{
|
|
<MudProgressLinear Indeterminate="true" Class="mb-4" />
|
|
<MudText>Parsing...</MudText>
|
|
}
|
|
else if (_parseResult == null)
|
|
{
|
|
<MudText Class="mud-text-secondary">Parse text to see results here</MudText>
|
|
}
|
|
else
|
|
{
|
|
<MudStack Spacing="3">
|
|
@* Errors *@
|
|
@if (_parseResult.Errors.Any())
|
|
{
|
|
@foreach (var error in _parseResult.Errors)
|
|
{
|
|
<MudAlert Severity="Severity.Error" Dense="true">@error</MudAlert>
|
|
}
|
|
}
|
|
|
|
@* Warnings *@
|
|
@if (_parseResult.Warnings.Any())
|
|
{
|
|
@foreach (var warning in _parseResult.Warnings)
|
|
{
|
|
<MudAlert Severity="Severity.Warning" Dense="true">@warning</MudAlert>
|
|
}
|
|
}
|
|
|
|
@* Detailed Parsing Issues *@
|
|
@if (_parseResult.Issues.Any())
|
|
{
|
|
<MudExpansionPanels Elevation="0" Class="mt-2">
|
|
<MudExpansionPanel Text="@($"Parsing Issues ({_parseResult.Issues.Count} found on {_parseResult.Issues.Select(i => i.LineNumber).Distinct().Count()} line(s))")"
|
|
Icon="@Icons.Material.Filled.Warning"
|
|
iconcolor="Color.Warning">
|
|
<MudTable Items="@_parseResult.Issues" Dense="true" Hover="true" Striped="true" sortmode="SortMode.Multiple">
|
|
<HeaderContent>
|
|
<MudTh>Line</MudTh>
|
|
<MudTh>Type</MudTh>
|
|
<MudTh>Content</MudTh>
|
|
<MudTh>Message</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Line">@context.LineNumber</MudTd>
|
|
<MudTd DataLabel="Type">
|
|
<MudChip T="string" Size="Size.Small" Color="@GetIssueTypeColor(context.IssueType)">
|
|
@context.IssueType
|
|
</MudChip>
|
|
</MudTd>
|
|
<MudTd DataLabel="Content">
|
|
<code style="font-size: 0.85em; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;" title="@context.LineContent">@context.LineContent</code>
|
|
</MudTd>
|
|
<MudTd DataLabel="Message">@context.Message</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudExpansionPanel>
|
|
</MudExpansionPanels>
|
|
}
|
|
|
|
@* Summary *@
|
|
@if (_parseResult.IsSuccess && _parseResult.TotalParsed > 0)
|
|
{
|
|
<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)
|
|
{
|
|
<text> (Skipped @(_parseResult.SkippedMSEventCount + _parseResult.SkippedHSEventCount) event occurrence(s) from other school level)</text>
|
|
}
|
|
</MudAlert>
|
|
}
|
|
|
|
@* Skipped Section Headers *@
|
|
@if ((_parseResult.SkippedMSSectionHeaders.Any() || _parseResult.SkippedHSSectionHeaders.Any()) && _parseResult.IsSuccess)
|
|
{
|
|
<MudExpansionPanels Elevation="0" Class="mt-2">
|
|
@if (_parseResult.SkippedMSSectionHeaders.Any())
|
|
{
|
|
<MudExpansionPanel Text="@($"Skipped MS Section Headers ({_parseResult.SkippedMSSectionHeaders.Count})")"
|
|
Icon="@Icons.Material.Filled.Info"
|
|
iconcolor="Color.Info">
|
|
<MudList T="string">
|
|
@foreach (var header in _parseResult.SkippedMSSectionHeaders)
|
|
{
|
|
<MudListItem T="string">
|
|
<MudText>@header</MudText>
|
|
</MudListItem>
|
|
}
|
|
</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>
|
|
}
|
|
|
|
@* 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;
|
|
<MudText Typo="Typo.h6" Class="mt-4 mb-2">Parsed Locations (@allLocations.Count unique@(warningCount > 0 ? $", {warningCount} with warnings" : ""))</MudText>
|
|
<MudExpansionPanels Elevation="0">
|
|
<MudExpansionPanel Text="All Locations">
|
|
<MudList T="string">
|
|
@foreach (var location in allLocations)
|
|
{
|
|
var isLong = longLocations.Contains(location);
|
|
var hasDateTime = locationsWithDateTime.Contains(location);
|
|
var hasWarning = isLong || hasDateTime;
|
|
<MudListItem T="string">
|
|
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center">
|
|
@if (hasWarning)
|
|
{
|
|
<MudChip T="string" Color="Color.Warning" Size="Size.Small">Warning</MudChip>
|
|
}
|
|
<MudText Style="@(hasWarning ? "color: var(--mud-palette-warning);" : "")">@location</MudText>
|
|
</MudStack>
|
|
</MudListItem>
|
|
}
|
|
</MudList>
|
|
</MudExpansionPanel>
|
|
</MudExpansionPanels>
|
|
}
|
|
}
|
|
|
|
@* Parsed Occurrences List *@
|
|
@if (_parseResult.IsSuccess && _parseResult.Occurrences.Any())
|
|
{
|
|
<MudText Typo="Typo.h6" Class="mt-4 mb-2">Occurrences by Event:</MudText>
|
|
<MudExpansionPanels Elevation="0">
|
|
@foreach (var kvp in _parseResult.Occurrences.OrderBy(x => GetEventName(x.Key)))
|
|
{
|
|
<MudExpansionPanel Text="@GetEventName(kvp.Key)">
|
|
<MudTable Items="@kvp.Value" Dense="true" Hover="true" Striped="true">
|
|
<HeaderContent>
|
|
<MudTh>Name</MudTh>
|
|
<MudTh>Date</MudTh>
|
|
<MudTh>Time</MudTh>
|
|
<MudTh>Location</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd DataLabel="Name">@context.Name</MudTd>
|
|
<MudTd DataLabel="Date">@context.Date</MudTd>
|
|
<MudTd DataLabel="Time">@context.Time</MudTd>
|
|
<MudTd DataLabel="Location">@(context.Location ?? "-")</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
</MudExpansionPanel>
|
|
}
|
|
</MudExpansionPanels>
|
|
|
|
<MudStack Row="true" Spacing="2" Class="mt-4">
|
|
<MudButton
|
|
Variant="Variant.Filled"
|
|
Color="Color.Success"
|
|
StartIcon="@Icons.Material.Filled.Save"
|
|
OnClick="HandleSaveToDatabase"
|
|
Disabled="@_isSaving">
|
|
Save to Database
|
|
</MudButton>
|
|
<MudButton
|
|
Variant="Variant.Text"
|
|
OnClick="HandleClearResults">
|
|
Clear Results
|
|
</MudButton>
|
|
</MudStack>
|
|
}
|
|
else if (_parseResult.IsSuccess && _parseResult.TotalParsed == 0)
|
|
{
|
|
<MudAlert Severity="Severity.Info" Dense="true">
|
|
No occurrences were parsed from the text. Please check the format.
|
|
</MudAlert>
|
|
}
|
|
</MudStack>
|
|
}
|
|
</MudPaper>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
@code {
|
|
private string _inputText = string.Empty;
|
|
private EventOccurrenceParseResult? _parseResult;
|
|
private bool _isParsing = false;
|
|
private bool _isSaving = false;
|
|
|
|
private async Task HandleParse()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(_inputText))
|
|
{
|
|
Snackbar.Add("Please enter text to parse", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
_isParsing = true;
|
|
try
|
|
{
|
|
// Get EventDefinitions from database
|
|
var events = await Context.Events.ToListAsync();
|
|
|
|
// Parse the text
|
|
_parseResult = ParserService.ParseFromText(_inputText, events);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error parsing text: {ex.Message}", Severity.Error);
|
|
_parseResult = new EventOccurrenceParseResult
|
|
{
|
|
Errors = { $"Error: {ex.Message}" }
|
|
};
|
|
}
|
|
finally
|
|
{
|
|
_isParsing = false;
|
|
}
|
|
}
|
|
|
|
private void HandleClear()
|
|
{
|
|
_inputText = string.Empty;
|
|
_parseResult = null;
|
|
}
|
|
|
|
private void HandleClearResults()
|
|
{
|
|
_parseResult = null;
|
|
}
|
|
|
|
private async Task HandleSaveToDatabase()
|
|
{
|
|
if (_parseResult == null || !_parseResult.IsSuccess || _parseResult.TotalParsed == 0)
|
|
{
|
|
Snackbar.Add("No valid parsed occurrences to save", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
_isSaving = true;
|
|
try
|
|
{
|
|
var savedCount = 0;
|
|
foreach (var kvp in _parseResult.Occurrences)
|
|
{
|
|
foreach (var occurrence in kvp.Value)
|
|
{
|
|
// Add each occurrence to the database
|
|
await Context.EventOccurrences.AddAsync(occurrence);
|
|
savedCount++;
|
|
}
|
|
}
|
|
|
|
await Context.SaveChangesAsync();
|
|
Snackbar.Add($"Successfully saved {savedCount} occurrence(s) to database", Severity.Success);
|
|
|
|
// Navigate back to the calendar index after a short delay
|
|
await Task.Delay(1000);
|
|
NavigationManager.NavigateTo("/calendar/event-occurrences");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error saving to database: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_isSaving = false;
|
|
}
|
|
}
|
|
|
|
private string GetEventName(EventDefinition eventDefinition)
|
|
{
|
|
if (eventDefinition == EventDefinition.GeneralSchedule)
|
|
return "General Schedule";
|
|
if (eventDefinition == EventDefinition.MeetTheCandidates)
|
|
return "Meet the Candidates";
|
|
if (eventDefinition == EventDefinition.ChapterOfficerMeeting)
|
|
return "Chapter Officer Meeting";
|
|
if (eventDefinition == EventDefinition.VotingDelegateMeeting)
|
|
return "Voting Delegate Meeting";
|
|
if (eventDefinition == EventDefinition.SocialGathering)
|
|
return "Social Gathering";
|
|
return eventDefinition.Name;
|
|
}
|
|
|
|
private Color GetIssueTypeColor(ParsingIssueType issueType)
|
|
{
|
|
return issueType switch
|
|
{
|
|
ParsingIssueType.UnmatchedLine => Color.Info,
|
|
ParsingIssueType.MissingEventDefinition => Color.Warning,
|
|
ParsingIssueType.TimeParseFailure => Color.Error,
|
|
ParsingIssueType.DateParseFailure => Color.Error,
|
|
ParsingIssueType.InvalidFormat => Color.Error,
|
|
_ => Color.Default
|
|
};
|
|
}
|
|
|
|
}
|
|
|