@namespace WebApp.Components.Features.MeetingSchedule @using Core.Entities @using Core.Services @using Core.Utility @using WebApp.Services @using WebApp.Components.Shared.Components @using WebApp.Models @inject ITeamMeetingHistoryService TeamMeetingHistoryService @inject INotesService NotesService @inject INoteNamingService NoteNamingService @inject ISnackbar Snackbar @inject IDialogService DialogService @inject IMeetingScheduleDataService DataService @inject IMeetingScheduleStateService StateService @inject NavigationManager NavigationManager @implements IAsyncDisposable @if (_isLoading) { } else if (_meetingHistory == null) { Meeting history not found. } else { @* Navigation Header *@ @_meetingHistory.MeetingDate.ToString("MM/dd/yyyy") 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) { Meeting Notes @_meetingNote.Title @if (!string.IsNullOrWhiteSpace(_meetingNote.Content)) { @((MarkupString)MarkdownHelper.ToHtml(_meetingNote.Content)) } Edit Note } } @if (_meetingHistory != null) { Delete Load into Planner Edit Close } else { Close } @code { [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; [Parameter] public int MeetingHistoryId { get; set; } private TeamMeetingHistory? _meetingHistory; private Note? _meetingNote; private bool _isLoading = true; private bool _isDeleting = false; private Team[] _allTeams = []; private Student[] _allStudents = []; private CancellationTokenSource? _cancellationTokenSource; private bool _isDisposed = false; private bool IsActionDisabled => _isLoading || _isDeleting; private TeamMeetingHistory? _previousMeetingHistory; private TeamMeetingHistory? _nextMeetingHistory; protected override void OnInitialized() { _cancellationTokenSource = new CancellationTokenSource(); } protected override async Task OnInitializedAsync() { // Load all teams and students for the edit dialog _allTeams = await DataService.LoadTeamsAsync(); _allStudents = await DataService.LoadStudentsAsync(); await LoadMeetingHistory(); } private async Task LoadMeetingHistory() { if (_isDisposed) return; try { _meetingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(MeetingHistoryId); // Load note by title if meeting history exists if (_meetingHistory != null) { _meetingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(_meetingHistory.MeetingDate); // Load all meeting histories to find previous/next await LoadNavigationMeetings(); } } catch (TaskCanceledException) { // Component was disposed, ignore } catch (JSDisconnectedException) { // JS connection lost, ignore } catch (Exception ex) { if (!_isDisposed) { Snackbar.Add($"Error loading meeting history: {ex.Message}", Severity.Error); } } finally { if (!_isDisposed) { _isLoading = false; StateHasChanged(); } } } private async Task LoadNavigationMeetings() { if (_isDisposed || _meetingHistory == null) return; try { // Get all meeting histories ordered by date var allMeetings = (await TeamMeetingHistoryService.GetMeetingHistoriesAsync()) .OrderBy(m => m.MeetingDate) .ThenBy(m => m.Id) .ToList(); var currentIndex = allMeetings.FindIndex(m => m.Id == _meetingHistory.Id); if (currentIndex >= 0) { _previousMeetingHistory = currentIndex > 0 ? allMeetings[currentIndex - 1] : null; _nextMeetingHistory = currentIndex < allMeetings.Count - 1 ? allMeetings[currentIndex + 1] : null; } else { _previousMeetingHistory = null; _nextMeetingHistory = null; } } catch (TaskCanceledException) { // Component was disposed, ignore } catch (JSDisconnectedException) { // JS connection lost, ignore } catch (Exception ex) { if (!_isDisposed) { // Log error but don't show snackbar - navigation is not critical System.Diagnostics.Debug.WriteLine($"Error loading navigation meetings: {ex.Message}"); } } } private async Task NavigateToPrevious() { if (_previousMeetingHistory == null || _isDisposed) return; // Close current dialog and open new one with previous meeting ID var parameters = new DialogParameters { ["MeetingHistoryId"] = _previousMeetingHistory.Id }; var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; MudDialog.Close(); await DialogService.ShowAsync("Meeting History", parameters, options); } private async Task NavigateToNext() { if (_nextMeetingHistory == null || _isDisposed) return; // Close current dialog and open new one with next meeting ID var parameters = new DialogParameters { ["MeetingHistoryId"] = _nextMeetingHistory.Id }; var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; MudDialog.Close(); await DialogService.ShowAsync("Meeting History", parameters, options); } private async Task ConfirmDelete() { if (_isDisposed || _isDeleting || _meetingHistory == null) return; var result = await DialogService.ShowMessageBox( "Confirm Delete", $"Are you sure you want to delete the meeting history for {_meetingHistory.MeetingDate:MM/dd/yyyy}? This action cannot be undone.", yesText: "Delete", cancelText: "Cancel"); if (result == true) { await DeleteMeetingHistory(); } } private async Task DeleteMeetingHistory() { if (_isDisposed || _isDeleting || _meetingHistory == null) return; try { _isDeleting = true; StateHasChanged(); await TeamMeetingHistoryService.DeleteMeetingHistoryAsync(MeetingHistoryId); if (!_isDisposed) { Snackbar.Add("Meeting history deleted successfully", Severity.Success); MudDialog.Close(DialogResult.Ok(true)); } } catch (TaskCanceledException) { // Component was disposed, ignore } catch (JSDisconnectedException) { // JS connection lost, ignore } catch (Exception ex) { if (!_isDisposed) { Snackbar.Add($"Error deleting meeting history: {ex.Message}", Severity.Error); _isDeleting = false; StateHasChanged(); } } } private async Task ViewNote() { if (_meetingNote == null) return; var parameters = new DialogParameters { ["NoteId"] = _meetingNote.Id }; var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; var dialog = await DialogService.ShowAsync("Meeting Notes", parameters, options); await dialog.Result; // Refresh meeting history to get updated note await LoadMeetingHistory(); } private async Task OpenEditDialog() { if (_meetingHistory == null || _isDisposed) return; var parameters = new DialogParameters { ["MeetingHistoryId"] = _meetingHistory.Id, ["AllTeams"] = _allTeams, ["AllStudents"] = _allStudents, ["ScheduledTeams"] = new List(), // Not used when editing ["AbsentStudents"] = new List() // Not used when editing }; var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true, CloseButton = true }; var dialog = await DialogService.ShowAsync("Edit Meeting History", parameters, options); var result = await dialog.Result; // Refresh meeting history if dialog was saved if (!result.Canceled && !_isDisposed) { await LoadMeetingHistory(); } } private List GetAllStudentsFromTeams() { if (_meetingHistory == null) return []; var allStudents = new List(); var studentIds = new HashSet(); foreach (var team in _meetingHistory.Teams) { foreach (var student in team.Students) { if (!studentIds.Contains(student.Id)) { studentIds.Add(student.Id); allStudents.Add(student); } } } return allStudents; } private async Task LoadIntoPlanner() { if (_isDisposed || _meetingHistory == null) return; try { // Get all teams and students from database var allTeams = await DataService.LoadTeamsAsync(); var allStudents = await DataService.LoadStudentsAsync(); // Match teams from history to all teams by ID for reference equality var historyTeamIds = _meetingHistory.Teams.Select(t => t.Id).ToHashSet(); var scheduledTeams = allTeams.Where(t => historyTeamIds.Contains(t.Id)); // Calculate absent students (all students not in the meeting history's student list) var presentStudentIds = _meetingHistory.Students.Select(s => s.Id).ToHashSet(); var absentStudents = allStudents.Where(s => !presentStudentIds.Contains(s.Id)); // Save state to localStorage await StateService.SaveScheduledTeamsAsync(scheduledTeams); await StateService.SaveAbsentStudentsAsync(absentStudents); // Clear extended teams and excluded students when loading from history await StateService.SaveExtendedTeamsAsync([]); await StateService.SaveExcludedStudentsAsync(new Dictionary<(int teamId, int timeSlotIndex, int studentId), bool>()); // Close dialog and navigate to planner MudDialog.Close(); NavigationManager.NavigateTo("/meeting-schedule"); if (!_isDisposed) { Snackbar.Add($"Loaded meeting from {_meetingHistory.MeetingDate:MM/dd/yyyy} into planner", Severity.Success); } } catch (TaskCanceledException) { // Component was disposed, ignore } catch (JSDisconnectedException) { // JS connection lost, ignore } catch (Exception ex) { if (!_isDisposed) { Snackbar.Add($"Error loading meeting into planner: {ex.Message}", Severity.Error); } } } private void Close() { if (_isDisposed) return; MudDialog.Close(); } public async ValueTask DisposeAsync() { if (!_isDisposed) { _isDisposed = true; _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } await ValueTask.CompletedTask; } }