Files
chapter-organizer/WebApp/Components/Features/Calendar/Index.razor
T
poprhythm 37e82646b8 Enhance calendar import functionality with null safety checks
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.
2026-01-10 19:08:11 -05:00

284 lines
10 KiB
Plaintext

@page "/calendar"
@attribute [Authorize]
@using WebApp.Models
@using WebApp.Services
@using Heron.MudCalendar
@using Microsoft.Extensions.Logging
@using WebApp.Authentication
@inject IEventOccurrenceService EventOccurrenceService
@inject ILogger<Index> Logger
@inject IDialogService DialogService
<PageHeader Title="Event Calendar" Description="View competition schedules and event occurrences" Icon="@AppIcons.EventCalendar">
<ActionButtons>
<MudButton StartIcon="@Icons.Material.Filled.ImportExport" Href="calendar/event-occurrences/import" Variant="Variant.Filled" Color="Color.Primary">Import</MudButton>
<AuthorizeView Roles="@AuthRoles.Administrator">
<MudButton StartIcon="@Icons.Material.Filled.AdminPanelSettings" Href="calendar/admin" Variant="Variant.Outlined" Color="Color.Default">Admin</MudButton>
</AuthorizeView>
</ActionButtons>
</PageHeader>
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
@if (_calendarItems == null)
{
<MudProgressLinear Indeterminate="true" />
<MudText>Loading calendar events...</MudText>
}
else
{
<MudStack Spacing="2">
<MudButtonGroup Variant="Variant.Outlined" Size="Size.Small">
<MudButton Color="@(_currentView == CalendarView.Month ? Color.Primary : Color.Default)"
OnClick="() => { _currentView = CalendarView.Month; StateHasChanged(); }">
Month
</MudButton>
<MudButton Color="@(_currentView == CalendarView.Day ? Color.Primary : Color.Default)"
OnClick="() => { _currentView = CalendarView.Day; StateHasChanged(); }">
Day
</MudButton>
</MudButtonGroup>
<MudCalendar T="CalendarEventItem"
Items="_calendarItems"
View="_currentView"
CurrentDay="@_calendarDate"
Class="event-calendar"
ItemClicked="OnItemClicked">
<MonthTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div class="d-flex gap-1">
<MudIcon Icon="@Icons.Material.Filled.Circle" Color="Color.Secondary" Size="Size.Small"/>
<div>@context.Text</div>
</div>
@* </MudTooltip> *@
</MonthTemplate>
<WeekTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div style="width: 100%; height: 100%;">
@context.Text
</div>
@* </MudTooltip> *@
</WeekTemplate>
<DayTemplate>
@* <MudTooltip Text="@GetEventTooltip(context)"> *@
<div>@context.Text</div>
@* </MudTooltip> *@
</DayTemplate>
</MudCalendar>
</MudStack>
}
</MudPaper>
@code {
private List<CalendarEventItem>? _calendarItems;
private DateTime _calendarDate = DateTime.Today;
private CalendarView _currentView = CalendarView.Month;
protected override async Task OnInitializedAsync()
{
await LoadCalendarEvents();
}
private async Task LoadCalendarEvents()
{
try
{
Logger.LogInformation("Loading calendar events");
var occurrences = await EventOccurrenceService.GetEventOccurrencesAsync();
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<CalendarEventItem>();
foreach (var occ in eventOccurrences)
{
try
{
if (string.IsNullOrEmpty(occ.Name))
{
Logger.LogWarning("Occurrence with Id={Id} has null or empty Name", occ.Id);
}
// Get student first names for this event definition
var studentFirstNames = occ.EventDefinition != null
? StudentFirstNames(occ.EventDefinition, teamsByEventId)
: new List<string>();
var calendarItem = new CalendarEventItem(occ, studentFirstNames);
items.Add(calendarItem);
}
catch (Exception ex)
{
Logger.LogError(ex, "Error creating CalendarEventItem for occurrence Id={Id}, Name={Name}",
occ?.Id, occ?.Name);
// Continue processing other items
}
}
_calendarItems = items;
Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences",
_calendarItems.Count, eventOccurrences.Count());
// Find the next date with events
_calendarDate = GetNextDateWithEvents();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading calendar events");
_calendarItems = new List<CalendarEventItem>();
}
finally
{
StateHasChanged();
}
}
private static List<string> StudentFirstNames(EventDefinition ed, Dictionary<int, List<Team>> teamsByEventId)
{
var studentFirstNames = new List<string>();
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
{
if (_calendarItems == null || !_calendarItems.Any())
{
Logger.LogDebug("No calendar items available, returning today's date");
return DateTime.Today;
}
var today = DateTime.Today;
var nextEvent = _calendarItems
.Where(item =>
{
try
{
return item.Start.Date >= today;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Error checking item date, skipping item");
return false;
}
})
.OrderBy(item => item.Start)
.FirstOrDefault();
if (nextEvent != null)
{
return nextEvent.Start.Date;
}
// Fallback to first event if no future events
var firstEvent = _calendarItems
.OrderBy(item => item.Start)
.FirstOrDefault();
return firstEvent != null ? firstEvent.Start.Date : DateTime.Today;
}
catch (Exception ex)
{
Logger.LogError(ex, "Error in GetNextDateWithEvents");
return DateTime.Today;
}
}
private string GetEventTooltip(CalendarEventItem item)
{
var parts = new List<string>();
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<EventOccurrenceDetailsDialog>("Event Details", parameters, options);
}
}