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); +}