@namespace WebApp.Components.Features.MeetingSchedule @using Core.Entities @using Core.Calculation @using Core.Services @using WebApp.Services @using WebApp.Components.Shared.Components @using WebApp.Components.Features.Teams.Components @using WebApp.Components.Features.Students.Components @using WebApp.Models @inject ITeamMeetingHistoryService TeamMeetingHistoryService @inject INotesService NotesService @inject INoteNamingService NoteNamingService @inject ISnackbar Snackbar @inject IDialogService DialogService @implements IAsyncDisposable @if (_isLoading) { } else { @(_isEditMode ? "Edit Meeting History" : "Save Meeting History") } Cancel @if (_isSaving) { Saving... } else { Save } @code { [CascadingParameter] IMudDialogInstance MudDialog { get; set; } = null!; [Parameter] public IEnumerable ScheduledTeams { get; set; } = []; [Parameter] public IEnumerable AbsentStudents { get; set; } = []; [Parameter] public IEnumerable AllTeams { get; set; } = []; [Parameter] public IEnumerable AllStudents { get; set; } = []; [Parameter] public int? MeetingHistoryId { get; set; } private DateTime? _meetingDate = DateTime.Today; private IEnumerable _selectedTeams = []; private IEnumerable _selectedStudents = []; private string _noteTitle = ""; private string _noteContent = ""; private bool _isLoading = true; private bool _isSaving = false; private bool _isEditMode = false; private CancellationTokenSource? _cancellationTokenSource; private bool _isDisposed = false; protected override void OnInitialized() { _cancellationTokenSource = new CancellationTokenSource(); } protected override async Task OnInitializedAsync() { await LoadInitialData(); } private async Task LoadInitialData() { if (_isDisposed) return; try { // If editing, load existing meeting history if (MeetingHistoryId.HasValue) { _isEditMode = true; var existingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(MeetingHistoryId.Value); if (existingHistory != null) { _meetingDate = existingHistory.MeetingDate; // Match teams by ID to ensure reference equality with MudToggleGroup var selectedTeamIds = existingHistory.Teams.Select(t => t.Id).ToHashSet(); 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(); 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); if (existingNote != null) { _noteContent = existingNote.Content ?? ""; } } } else { // Initialize selected teams from scheduled teams _selectedTeams = ScheduledTeams; // Initialize selected students (all students except absent ones) var absentStudentIds = AbsentStudents.Select(s => s.Id).ToHashSet(); _selectedStudents = AllStudents.Where(s => !absentStudentIds.Contains(s.Id)); } // Generate default note title if meeting date is set if (_meetingDate.HasValue) { _noteTitle = NoteNamingService.GetMeetingNoteTitle(_meetingDate.Value); } } catch (TaskCanceledException) { // Component was disposed, ignore } catch (JSDisconnectedException) { // JS connection lost, ignore } catch (Exception ex) { if (!_isDisposed) { Snackbar.Add($"Error loading data: {ex.Message}", Severity.Error); } } finally { if (!_isDisposed) { _isLoading = false; StateHasChanged(); } } } private bool IsSaveDisabled => _isSaving || _isLoading; protected override async Task OnParametersSetAsync() { if (_meetingDate.HasValue && string.IsNullOrEmpty(_noteTitle)) { _noteTitle = NoteNamingService.GetMeetingNoteTitle(_meetingDate.Value); } await base.OnParametersSetAsync(); } private async Task OnMeetingDateChanged(DateTime? date) { _meetingDate = date; if (date.HasValue) { _noteTitle = NoteNamingService.GetMeetingNoteTitle(date.Value); } await InvokeAsync(StateHasChanged); } private void OnTeamsChanged(IEnumerable teams) { _selectedTeams = teams; StateHasChanged(); } private void OnStudentsChanged(IEnumerable students) { _selectedStudents = students; StateHasChanged(); } private async Task Save() { if (_isDisposed || _isSaving) return; if (!_meetingDate.HasValue) { Snackbar.Add("Please select a meeting date", Severity.Warning); 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) { var dateOnly = _meetingDate.Value.Date; // GetMeetingHistoriesAsync: startDate >= date, endDate < (endDate.Date + 1 day) // To get meetings for a single day, pass dateOnly as startDate and dateOnly as endDate // This becomes: MeetingDate >= dateOnly AND MeetingDate < (dateOnly + 1 day) = just that day var existingMeetings = await TeamMeetingHistoryService.GetMeetingHistoriesAsync(dateOnly, dateOnly); if (existingMeetings.Any()) { // Show confirmation dialog var confirmResult = await DialogService.ShowMessageBox( "Overwrite Meeting?", "A meeting already exists for this date. Overwrite it?", yesText: "Overwrite", cancelText: "Cancel"); if (confirmResult != true) { return; // User cancelled } // User confirmed, switch to edit mode var existingMeeting = existingMeetings.First(); _isEditMode = true; MeetingHistoryId = existingMeeting.Id; // Load existing meeting data var existingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(existingMeeting.Id); if (existingHistory != null) { // 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)); // 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)); // Load existing note if available var existingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(existingHistory.MeetingDate); if (existingNote != null) { _noteContent = existingNote.Content ?? ""; } } } } try { _isSaving = true; StateHasChanged(); // Create or update note if note content is provided Note? note = null; if (!string.IsNullOrWhiteSpace(_noteContent)) { // Use the naming service to get the meeting note title var noteTitle = NoteNamingService.GetMeetingNoteTitle(_meetingDate.Value); // Check if note already exists var existingNote = await NotesService.GetNotesAsync(includeDeleted: false); note = existingNote.FirstOrDefault(n => n.Title == noteTitle); if (note != null) { // Update existing note note.Content = _noteContent; note = await NotesService.UpdateNoteAsync(note); } else { // Create new note note = new Note { Title = noteTitle, Content = _noteContent }; note = await NotesService.CreateNoteAsync(note); } } // Create or update meeting history TeamMeetingHistory meetingHistory; if (_isEditMode && MeetingHistoryId.HasValue) { // Update existing meeting history var existingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(MeetingHistoryId.Value); if (existingHistory == null) { Snackbar.Add("Meeting history not found", Severity.Error); _isSaving = false; StateHasChanged(); return; } // 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); if (!_isDisposed) { Snackbar.Add($"Meeting history updated for {_meetingDate.Value:MM/dd/yyyy}", Severity.Success); } } else { // Create new meeting history meetingHistory = new TeamMeetingHistory { MeetingDate = _meetingDate.Value, Teams = _selectedTeams.ToList(), Students = _selectedStudents.ToList() }; await TeamMeetingHistoryService.CreateMeetingHistoryAsync(meetingHistory); if (!_isDisposed) { Snackbar.Add($"Meeting history saved for {_meetingDate.Value:MM/dd/yyyy}", Severity.Success); } } if (!_isDisposed) { 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 saving meeting history: {ex.Message}", Severity.Error); _isSaving = false; StateHasChanged(); } } } private void Cancel() { if (_isDisposed) return; MudDialog.Cancel(); } public async ValueTask DisposeAsync() { if (!_isDisposed) { _isDisposed = true; _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); _cancellationTokenSource = null; } await ValueTask.CompletedTask; } }