ecd6173a44
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.
412 lines
18 KiB
Plaintext
412 lines
18 KiB
Plaintext
@page "/calendar/event-occurrences/import"
|
|
@attribute [Authorize]
|
|
@using Core.Models
|
|
@using Core.Services
|
|
@using Microsoft.EntityFrameworkCore
|
|
@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.SkippedEventCount > 0)
|
|
{
|
|
<text> (Skipped @_parseResult.SkippedEventCount event occurrence(s) from other school level)</text>
|
|
}
|
|
</MudAlert>
|
|
}
|
|
|
|
@* Skipped Section Headers *@
|
|
@if (_parseResult.SkippedSectionHeaders.Any() && _parseResult.IsSuccess)
|
|
{
|
|
<MudExpansionPanels Elevation="0" Class="mt-2">
|
|
<MudExpansionPanel Text="@($"Skipped Section Headers ({_parseResult.SkippedSectionHeaders.Count})")"
|
|
Icon="@Icons.Material.Filled.Info"
|
|
iconcolor="Color.Info">
|
|
<MudList T="string">
|
|
@foreach (var header in _parseResult.SkippedSectionHeaders)
|
|
{
|
|
<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;
|
|
var skippedCount = 0;
|
|
|
|
foreach (var kvp in _parseResult.Occurrences)
|
|
{
|
|
foreach (var occurrence in kvp.Value)
|
|
{
|
|
// Check for duplicates before adding
|
|
var isDuplicate = await IsDuplicate(occurrence);
|
|
if (isDuplicate)
|
|
{
|
|
skippedCount++;
|
|
continue;
|
|
}
|
|
|
|
// Add each occurrence to the database
|
|
await Context.EventOccurrences.AddAsync(occurrence);
|
|
savedCount++;
|
|
}
|
|
}
|
|
|
|
await Context.SaveChangesAsync();
|
|
|
|
var message = $"Successfully saved {savedCount} occurrence(s) to database";
|
|
if (skippedCount > 0)
|
|
{
|
|
message += $" ({skippedCount} duplicate(s) skipped)";
|
|
}
|
|
Snackbar.Add(message, Severity.Success);
|
|
|
|
// Clear input and output instead of navigating
|
|
_inputText = string.Empty;
|
|
_parseResult = null;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add($"Error saving to database: {ex.Message}", Severity.Error);
|
|
}
|
|
finally
|
|
{
|
|
_isSaving = false;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> IsDuplicate(EventOccurrence occurrence)
|
|
{
|
|
// Check if an occurrence with the same name, date, time, location, and event definition already exists
|
|
var query = Context.EventOccurrences
|
|
.Where(eo => eo.Name == occurrence.Name &&
|
|
eo.StartTime.Date == occurrence.StartTime.Date &&
|
|
eo.Time == occurrence.Time &&
|
|
eo.Location == occurrence.Location);
|
|
|
|
// Match by EventDefinitionId if it exists, otherwise match by SpecialEventType
|
|
if (occurrence.EventDefinitionId.HasValue)
|
|
{
|
|
query = query.Where(eo => eo.EventDefinitionId == occurrence.EventDefinitionId);
|
|
}
|
|
else if (!string.IsNullOrEmpty(occurrence.SpecialEventType))
|
|
{
|
|
query = query.Where(eo => eo.SpecialEventType == occurrence.SpecialEventType);
|
|
}
|
|
else
|
|
{
|
|
// If neither EventDefinitionId nor SpecialEventType is set, match by both being null/empty
|
|
query = query.Where(eo => eo.EventDefinitionId == null && string.IsNullOrEmpty(eo.SpecialEventType));
|
|
}
|
|
|
|
return await query.AnyAsync();
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
}
|
|
|