From 675f04afec7304718370d218123515be2b22aeb1 Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Tue, 27 Jan 2026 22:38:52 -0500 Subject: [PATCH] Add CalendarService and integrate calendar functionality into components This commit introduces the CalendarService, which provides methods for retrieving all calendar items and upcoming events. The service is integrated into various components, including the Calendar and Home pages, enhancing the calendar functionality by allowing users to view upcoming events and meeting histories. Additionally, the Index.razor component is updated to parse query parameters for date selection, improving user experience. The layout of the MeetingHistoryDetailDialog and SaveMeetingHistoryDialog components is also refined for better organization and responsiveness. These changes collectively enhance the calendar feature and improve the overall user interface in managing events and meetings. --- .../Components/Features/Calendar/Index.razor | 145 ++------------ .../MeetingHistoryDetailDialog.razor | 69 ++++--- .../SaveMeetingHistoryDialog.razor | 82 +++++--- WebApp/Components/Pages/Home.razor | 189 ++++++++++++++++-- WebApp/Components/Shared/Layout/NavMenu.razor | 10 +- WebApp/Program.cs | 1 + WebApp/Services/CalendarService.cs | 144 +++++++++++++ WebApp/Services/ICalendarService.cs | 25 +++ 8 files changed, 452 insertions(+), 213 deletions(-) create mode 100644 WebApp/Services/CalendarService.cs create mode 100644 WebApp/Services/ICalendarService.cs diff --git a/WebApp/Components/Features/Calendar/Index.razor b/WebApp/Components/Features/Calendar/Index.razor index 490aa31..b530457 100644 --- a/WebApp/Components/Features/Calendar/Index.razor +++ b/WebApp/Components/Features/Calendar/Index.razor @@ -5,9 +5,7 @@ @using Heron.MudCalendar @using Microsoft.Extensions.Logging @using WebApp.Authentication -@using Core.Utility -@inject IEventOccurrenceService EventOccurrenceService -@inject ITeamMeetingHistoryService TeamMeetingHistoryService +@inject ICalendarService CalendarService @inject ILogger Logger @inject IDialogService DialogService @@ -72,8 +70,17 @@ private DateTime _calendarDate = DateTime.Today; private CalendarView _currentView = CalendarView.Month; + [SupplyParameterFromQuery] + private string? Date { get; set; } + protected override async Task OnInitializedAsync() { + // Parse date from query parameter if provided + if (!string.IsNullOrEmpty(Date) && DateTime.TryParse(Date, out var parsedDate)) + { + _calendarDate = parsedDate.Date; + } + await LoadCalendarEvents(); } @@ -82,91 +89,8 @@ 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); - - List items = []; - - // Add event occurrences - 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 && teamsByEventId.TryGetValue(occ.EventDefinition.Id, out var teams) - ? TeamStudentNameFormatter.FormatStudentListForEvent( - occ.EventDefinition, - teams, - new TeamStudentNameFormatter.FormatOptions - { - CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Star, - Ordering = TeamStudentNameFormatter.OrderingStyle.Alphabetical - }) - : []; - - var calendarEventItem = new CalendarEventItem(occ, studentFirstNames); - items.Add(new CalendarItemWrapper(calendarEventItem)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error creating CalendarEventItem for occurrence Id={Id}, Name={Name}", - occ?.Id, occ?.Name); - // Continue processing other items - } - } - - // Load and add meeting histories - try - { - Logger.LogInformation("Loading meeting histories"); - var meetingHistories = await TeamMeetingHistoryService.GetMeetingHistoriesAsync(); - - foreach (var meetingHistory in meetingHistories) - { - try - { - var calendarMeetingItem = new CalendarMeetingItem(meetingHistory); - items.Add(new CalendarItemWrapper(calendarMeetingItem)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error creating CalendarMeetingItem for meeting history Id={Id}, Date={Date}", - meetingHistory?.Id, meetingHistory?.MeetingDate); - // Continue processing other items - } - } - - Logger.LogInformation("Added {Count} meeting histories to calendar", meetingHistories.Count()); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error loading meeting histories"); - // Continue - don't fail the entire calendar load if meetings fail - } - - _calendarItems = items; - Logger.LogInformation("Created {Count} calendar items from {OccurrenceCount} occurrences and meetings", - _calendarItems.Count, eventOccurrences.Count()); - - // Find the next date with events - _calendarDate = GetNextDateWithEvents(); + _calendarItems = await CalendarService.GetAllCalendarItemsAsync(); + Logger.LogInformation("Loaded {Count} calendar items", _calendarItems.Count); } catch (Exception ex) { @@ -180,51 +104,6 @@ } - 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 nextItem = _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 (nextItem != null) - { - return nextItem.Start.Date; - } - - // Fallback to first item if no future items - var firstItem = _calendarItems - .OrderBy(item => item.Start) - .FirstOrDefault(); - - return firstItem != null ? firstItem.Start.Date : DateTime.Today; - } - catch (Exception ex) - { - Logger.LogError(ex, "Error in GetNextDateWithEvents"); - return DateTime.Today; - } - } private string GetEventTooltip(CalendarItemWrapper wrapper) { diff --git a/WebApp/Components/Features/MeetingSchedule/MeetingHistoryDetailDialog.razor b/WebApp/Components/Features/MeetingSchedule/MeetingHistoryDetailDialog.razor index af94d5c..99d3fe6 100644 --- a/WebApp/Components/Features/MeetingSchedule/MeetingHistoryDetailDialog.razor +++ b/WebApp/Components/Features/MeetingSchedule/MeetingHistoryDetailDialog.razor @@ -47,39 +47,42 @@ - Teams That Met (@_meetingHistory.Teams.Count) - - - @foreach (var team in _meetingHistory.Teams.OrderByEventFormatFirst().ThenBy(e => e.ToString())) - { - @team.ToString() - } - - - - - - Students (@GetAllStudentsFromTeams().Count) - - - @{ - var presentStudentIds = _meetingHistory.Students.Select(s => s.Id).ToHashSet(); - var allStudents = GetAllStudentsFromTeams().OrderBy(s => s.FirstName); - } - @foreach (var student in allStudents) - { - var isPresent = presentStudentIds.Contains(student.Id); - - @student.FirstNameLastName - - } - - + + + Teams That Met (@_meetingHistory.Teams.Count) + + + @foreach (var team in _meetingHistory.Teams.OrderByEventFormatFirst().ThenBy(e => e.ToString())) + { + @team.ToString() + } + + + + + Students (@GetAllStudentsFromTeams().Count) + + + @{ + var presentStudentIds = _meetingHistory.Students.Select(s => s.Id).ToHashSet(); + var allStudents = GetAllStudentsFromTeams().OrderBy(s => s.FirstName); + } + @foreach (var student in allStudents) + { + var isPresent = presentStudentIds.Contains(student.Id); + + @student.FirstNameLastName + + } + + + + @if (_meetingNote != null) { diff --git a/WebApp/Components/Features/MeetingSchedule/SaveMeetingHistoryDialog.razor b/WebApp/Components/Features/MeetingSchedule/SaveMeetingHistoryDialog.razor index 2a4bde2..4be7928 100644 --- a/WebApp/Components/Features/MeetingSchedule/SaveMeetingHistoryDialog.razor +++ b/WebApp/Components/Features/MeetingSchedule/SaveMeetingHistoryDialog.razor @@ -33,23 +33,26 @@ - - - - - - - - - + + + + + + + + + + + + @@ -149,11 +152,28 @@ // Match teams by ID to ensure reference equality with MudToggleGroup var selectedTeamIds = existingHistory.Teams.Select(t => t.Id).ToHashSet(); - _selectedTeams = AllTeams.Where(t => selectedTeamIds.Contains(t.Id)); + var matchedTeams = AllTeams.Where(t => selectedTeamIds.Contains(t.Id)).ToList(); + + // If we couldn't match all teams from AllTeams, use the teams from existing history + // This can happen if AllTeams is empty or doesn't contain all the teams + if (matchedTeams.Count != existingHistory.Teams.Count && AllTeams.Any()) + { + // Try to match what we can, but log a warning + System.Diagnostics.Debug.WriteLine($"Warning: Could not match all teams. Expected {existingHistory.Teams.Count}, matched {matchedTeams.Count}"); + } + _selectedTeams = matchedTeams.Any() ? matchedTeams : existingHistory.Teams; // Match students by ID to ensure reference equality with MudToggleGroup var selectedStudentIds = existingHistory.Students.Select(s => s.Id).ToHashSet(); - _selectedStudents = AllStudents.Where(s => selectedStudentIds.Contains(s.Id)); + var matchedStudents = AllStudents.Where(s => selectedStudentIds.Contains(s.Id)).ToList(); + + // If we couldn't match all students from AllStudents, use the students from existing history + if (matchedStudents.Count != existingHistory.Students.Count && AllStudents.Any()) + { + // Try to match what we can, but log a warning + System.Diagnostics.Debug.WriteLine($"Warning: Could not match all students. Expected {existingHistory.Students.Count}, matched {matchedStudents.Count}"); + } + _selectedStudents = matchedStudents.Any() ? matchedStudents : existingHistory.Students; // Load existing note if available var existingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(existingHistory.MeetingDate); @@ -247,6 +267,13 @@ return; } + // Validate that we have at least one team selected + if (!_selectedTeams.Any()) + { + Snackbar.Add("Please select at least one team", Severity.Warning); + return; + } + // Check if a meeting history already exists for this date (only when creating new, not editing) if (!_isEditMode) { @@ -344,10 +371,19 @@ return; } - meetingHistory = existingHistory; - meetingHistory.MeetingDate = _meetingDate.Value; - meetingHistory.Teams = _selectedTeams.ToList(); - meetingHistory.Students = _selectedStudents.ToList(); + // Ensure we have teams and students - use existing if selected lists are empty (fallback) + var teamsToSave = _selectedTeams.Any() ? _selectedTeams.ToList() : existingHistory.Teams.ToList(); + var studentsToSave = _selectedStudents.Any() ? _selectedStudents.ToList() : existingHistory.Students.ToList(); + + // Create a new meeting history object with the updated data + // Use IDs to ensure we're working with the correct entities + meetingHistory = new TeamMeetingHistory + { + Id = existingHistory.Id, + MeetingDate = _meetingDate.Value, + Teams = teamsToSave, + Students = studentsToSave + }; await TeamMeetingHistoryService.UpdateMeetingHistoryAsync(meetingHistory); diff --git a/WebApp/Components/Pages/Home.razor b/WebApp/Components/Pages/Home.razor index 91256dd..3fe682c 100644 --- a/WebApp/Components/Pages/Home.razor +++ b/WebApp/Components/Pages/Home.razor @@ -5,10 +5,14 @@ @using Core.Entities @using WebApp.Services @using WebApp.Components.Shared.Components +@using Heron.MudCalendar @inject IConfiguration Configuration @inject AppDbContext Context @inject INotesService NotesService @inject IDialogService DialogService +@inject ITeamMeetingHistoryService TeamMeetingHistoryService +@inject ICalendarService CalendarService +@inject NavigationManager Navigation @implements IAsyncDisposable @Configuration["ChapterSettings:Name"] - TSA Chapter Organizer @@ -124,13 +128,76 @@ else + NavigateUrl="/meeting-schedule"> + @if (_recentMeetings.Any()) + { + + + Recent Meetings + + @foreach (var meeting in _recentMeetings) + { +
+ + @meeting.MeetingDate.ToString("MMM d, yyyy") + +
+ } +
+
+ + +
+ + + View History + + +
+
+
+
+ } + else + { + Optimize meeting times + } +
+ NavigateUrl="/calendar"> + @if (_nextCalendarItems.Any()) + { + Next Events + + @foreach (var item in _nextCalendarItems) + { + var itemDate = item.Start.Date; + var itemName = item.ItemType == CalendarItemType.Event && item.EventItem != null + ? (item.EventItem.EventDefinition?.ShortName ?? item.EventItem.EventOccurrenceData?.Name ?? "Event") + : "Team Meeting"; + var dateStr = itemDate.ToString("yyyy-MM-dd"); +
+ + @itemName - @itemDate.ToString("MMM d, yyyy") + +
+ } +
+ } + else + { + Conference schedules + } +
@@ -150,21 +217,6 @@ else NavigateUrl="/students/teams"/> - - Team Building - - - - - - - Chapter Data @@ -189,6 +241,21 @@ else Caption="@($"{_teamEventsCount} Team | {_individualEventsCount} Individual")" NavigateUrl="/events" /> + + + Team Building + + + + + + } @code { @@ -200,6 +267,9 @@ else private int _teamCount; private int _individualTeamsCount; private int _groupTeamsCount; + private int _meetingHistoryCount; + private List _recentMeetings = []; + private List _nextCalendarItems = []; private List _pinnedNotes = []; private CancellationTokenSource? _cancellationTokenSource; private bool _isDisposed = false; @@ -216,6 +286,8 @@ else { await LoadStatistics(); await LoadPinnedNotes(); + await LoadMeetingHistoryCount(); + await LoadNextCalendarItem(); } private async Task HandleNoteChanged() @@ -285,6 +357,85 @@ else _groupTeamsCount = contextTeams.Count(e => e.Event.EventFormat == EventFormat.Team); } + private async Task LoadMeetingHistoryCount() + { + if (_isDisposed) return; + + try + { + var meetingHistories = await TeamMeetingHistoryService.GetMeetingHistoriesAsync(); + var historiesList = meetingHistories.ToList(); + _meetingHistoryCount = historiesList.Count; + + // Get the 3 most recent meetings in descending order + _recentMeetings = historiesList + .OrderByDescending(m => m.MeetingDate) + .ThenByDescending(m => m.Id) + .Take(3) + .ToList(); + } + catch (TaskCanceledException) + { + // Component was disposed, ignore + } + catch (JSDisconnectedException) + { + // JS connection lost, ignore + } + catch (Exception) + { + // Error loading meeting history - ignore + _meetingHistoryCount = 0; + _recentMeetings = []; + } + } + + private async Task ShowMeetingDetails(TeamMeetingHistory meeting) + { + var parameters = new DialogParameters + { + ["MeetingHistoryId"] = meeting.Id + }; + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + CloseButton = true, + MaxWidth = MaxWidth.Large, + FullWidth = true + }; + + await DialogService.ShowAsync("Meeting Details", parameters, options); + } + + private async Task LoadNextCalendarItem() + { + if (_isDisposed) return; + + try + { + _nextCalendarItems = await CalendarService.GetUpcomingCalendarItemsAsync(3); + } + catch (TaskCanceledException) + { + // Component was disposed, ignore + } + catch (JSDisconnectedException) + { + // JS connection lost, ignore + } + catch (Exception) + { + // Error loading calendar items - ignore + _nextCalendarItems = []; + } + } + + private void NavigateToCalendar(string date) + { + Navigation.NavigateTo($"/calendar?date={date}"); + } + public async ValueTask DisposeAsync() { if (!_isDisposed) diff --git a/WebApp/Components/Shared/Layout/NavMenu.razor b/WebApp/Components/Shared/Layout/NavMenu.razor index 6b8e4fc..809f890 100644 --- a/WebApp/Components/Shared/Layout/NavMenu.razor +++ b/WebApp/Components/Shared/Layout/NavMenu.razor @@ -18,16 +18,16 @@ Registration - - Event Ranking - Team Assignment - - Students Events + + Event Ranking + Team Assignment + + Notes diff --git a/WebApp/Program.cs b/WebApp/Program.cs index fb683c8..fb9ca34 100644 --- a/WebApp/Program.cs +++ b/WebApp/Program.cs @@ -195,6 +195,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/WebApp/Services/CalendarService.cs b/WebApp/Services/CalendarService.cs new file mode 100644 index 0000000..9d4e0a9 --- /dev/null +++ b/WebApp/Services/CalendarService.cs @@ -0,0 +1,144 @@ +using Core.Entities; +using Core.Utility; +using WebApp.Models; + +namespace WebApp.Services; + +/// +/// Service for calendar-related operations. +/// +public class CalendarService : ICalendarService +{ + private readonly IEventOccurrenceService _eventOccurrenceService; + private readonly ITeamMeetingHistoryService _teamMeetingHistoryService; + + public CalendarService( + IEventOccurrenceService eventOccurrenceService, + ITeamMeetingHistoryService teamMeetingHistoryService) + { + _eventOccurrenceService = eventOccurrenceService; + _teamMeetingHistoryService = teamMeetingHistoryService; + } + + public async Task> GetAllCalendarItemsAsync() + { + var items = new List(); + await AddEventItemsAsync(items, includeStudentNames: true); + await AddMeetingItemsAsync(items); + return items; + } + + public async Task> GetUpcomingCalendarItemsAsync(int count = 3) + { + var today = DateTime.Today; + var items = new List(); + await AddEventItemsAsync(items, startDate: today, includeStudentNames: false); + await AddMeetingItemsAsync(items, startDate: today); + + // Sort by date and take top N + return items + .OrderBy(item => item.Start) + .Take(count) + .ToList(); + } + + private async Task AddEventItemsAsync( + List items, + DateTime? startDate = null, + bool includeStudentNames = false) + { + try + { + var occurrences = await _eventOccurrenceService.GetEventOccurrencesAsync(); + var eventOccurrences = occurrences as EventOccurrence[] ?? occurrences.ToArray(); + + // Filter by start date if provided + if (startDate.HasValue) + { + eventOccurrences = eventOccurrences + .Where(occ => occ.StartTime.Date >= startDate.Value) + .ToArray(); + } + + Dictionary>? teamsByEventId = null; + if (includeStudentNames) + { + // 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 + teamsByEventId = await _eventOccurrenceService.GetTeamsByEventDefinitionIdsAsync(eventDefinitionIds); + } + + // Add event occurrences + foreach (var occ in eventOccurrences) + { + try + { + List? studentFirstNames = null; + if (includeStudentNames && occ.EventDefinition != null && teamsByEventId != null) + { + studentFirstNames = teamsByEventId.TryGetValue(occ.EventDefinition.Id, out var teams) + ? TeamStudentNameFormatter.FormatStudentListForEvent( + occ.EventDefinition, + teams, + new TeamStudentNameFormatter.FormatOptions + { + CaptainIndicator = TeamStudentNameFormatter.CaptainIndicatorStyle.Star, + Ordering = TeamStudentNameFormatter.OrderingStyle.Alphabetical + }) + : []; + } + + var calendarEventItem = new CalendarEventItem(occ, studentFirstNames); + items.Add(new CalendarItemWrapper(calendarEventItem)); + } + catch (Exception) + { + // Continue processing other items + } + } + } + catch (Exception) + { + // Continue - don't fail the entire calendar load if events fail + } + } + + private async Task AddMeetingItemsAsync( + List items, + DateTime? startDate = null) + { + try + { + var meetingHistories = await _teamMeetingHistoryService.GetMeetingHistoriesAsync(); + + IEnumerable meetings = meetingHistories; + if (startDate.HasValue) + { + meetings = meetings.Where(m => m.MeetingDate.Date >= startDate.Value); + } + + foreach (var meetingHistory in meetings) + { + try + { + var calendarMeetingItem = new CalendarMeetingItem(meetingHistory); + items.Add(new CalendarItemWrapper(calendarMeetingItem)); + } + catch (Exception) + { + // Continue processing other items + } + } + } + catch (Exception) + { + // Continue - don't fail the entire calendar load if meetings fail + } + } +} diff --git a/WebApp/Services/ICalendarService.cs b/WebApp/Services/ICalendarService.cs new file mode 100644 index 0000000..504084d --- /dev/null +++ b/WebApp/Services/ICalendarService.cs @@ -0,0 +1,25 @@ +using Core.Entities; +using WebApp.Models; + +namespace WebApp.Services; + +/// +/// Service for calendar-related operations. +/// +public interface ICalendarService +{ + /// + /// Gets all calendar items (events and meetings) for display in the calendar. + /// Includes student names for events. + /// + /// A list of CalendarItemWrapper objects ready for display. + Task> GetAllCalendarItemsAsync(); + + /// + /// Gets the next N upcoming calendar items (events and meetings) starting from today. + /// Returns CalendarItemWrapper objects ready for display. + /// + /// The maximum number of items to return. Default is 3. + /// A list of CalendarItemWrapper objects ready for display. + Task> GetUpcomingCalendarItemsAsync(int count = 3); +}