37e82646b8
This commit updates the Import.razor component to include a cast to non-nullable strings after filtering locations, ensuring that only valid strings are processed. Additionally, the Index.razor component is modified to handle null event definitions gracefully by providing an empty list of student first names when the event definition is null. These changes improve the robustness and reliability of the calendar import feature.
413 lines
18 KiB
Plaintext
413 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))
|
|
.Cast<string>() // Cast to non-nullable string after null check
|
|
.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
|
|
};
|
|
}
|
|
|
|
}
|
|
|