Enhance MeetingHistoryDetailDialog and SaveMeetingHistoryDialog with navigation and editing features

This commit introduces navigation buttons for previous and next meeting histories in the MeetingHistoryDetailDialog, improving user experience by allowing easy access to related meetings. Additionally, the SaveMeetingHistoryDialog is updated to support editing existing meeting histories, with logic to load existing data and prevent duplicate entries for the same date. The integration of TeamToggleSelector and StudentToggleSelector components streamlines team and student selection, enhancing the overall functionality of the meeting history management feature.
This commit is contained in:
2026-01-20 21:55:38 -05:00
parent ddb743847d
commit 455be30821
2 changed files with 268 additions and 48 deletions
@@ -10,6 +10,7 @@
@inject INoteNamingService NoteNamingService @inject INoteNamingService NoteNamingService
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IDialogService DialogService @inject IDialogService DialogService
@inject IMeetingScheduleDataService DataService
@implements IAsyncDisposable @implements IAsyncDisposable
<MudDialog> <MudDialog>
@@ -25,7 +26,28 @@
else else
{ {
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudText Typo="Typo.h6">Meeting Details</MudText> @* Navigation Header *@
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Spacing="2">
<MudTooltip Text="@(_previousMeetingHistory != null ? $"Previous: {_previousMeetingHistory.MeetingDate:MM/dd/yyyy}" : "No previous meeting")">
<span>
<MudIconButton Icon="@Icons.Material.Filled.ChevronLeft"
OnClick="NavigateToPrevious"
Disabled="@(_previousMeetingHistory == null || IsActionDisabled)"
Color="Color.Primary"
Size="Size.Medium" />
</span>
</MudTooltip>
<MudText Typo="Typo.h6" Class="flex-grow-1" Style="text-align: center;">@_meetingHistory.MeetingDate.ToString("MM/dd/yyyy")</MudText>
<MudTooltip Text="@(_nextMeetingHistory != null ? $"Next: {_nextMeetingHistory.MeetingDate:MM/dd/yyyy}" : "No next meeting")">
<span>
<MudIconButton Icon="@Icons.Material.Filled.ChevronRight"
OnClick="NavigateToNext"
Disabled="@(_nextMeetingHistory == null || IsActionDisabled)"
Color="Color.Primary"
Size="Size.Medium" />
</span>
</MudTooltip>
</MudStack>
<MudPaper Elevation="1" Class="pa-3"> <MudPaper Elevation="1" Class="pa-3">
<MudStack Spacing="2"> <MudStack Spacing="2">
@@ -106,6 +128,13 @@
Delete Delete
</MudButton> </MudButton>
<MudSpacer /> <MudSpacer />
<MudButton Variant="Variant.Text"
Color="Color.Primary"
OnClick="OpenEditDialog"
Disabled="@IsActionDisabled"
StartIcon="@Icons.Material.Filled.Edit">
Edit
</MudButton>
<MudButton OnClick="Close" Disabled="@IsActionDisabled">Close</MudButton> <MudButton OnClick="Close" Disabled="@IsActionDisabled">Close</MudButton>
} }
else else
@@ -127,9 +156,13 @@
private Note? _meetingNote; private Note? _meetingNote;
private bool _isLoading = true; private bool _isLoading = true;
private bool _isDeleting = false; private bool _isDeleting = false;
private Team[] _allTeams = [];
private Student[] _allStudents = [];
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false; private bool _isDisposed = false;
private bool IsActionDisabled => _isLoading || _isDeleting; private bool IsActionDisabled => _isLoading || _isDeleting;
private TeamMeetingHistory? _previousMeetingHistory;
private TeamMeetingHistory? _nextMeetingHistory;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -138,6 +171,9 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// Load all teams and students for the edit dialog
_allTeams = await DataService.LoadTeamsAsync();
_allStudents = await DataService.LoadStudentsAsync();
await LoadMeetingHistory(); await LoadMeetingHistory();
} }
@@ -153,6 +189,9 @@
if (_meetingHistory != null) if (_meetingHistory != null)
{ {
_meetingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(_meetingHistory.MeetingDate); _meetingNote = await TeamMeetingHistoryService.GetMeetingNoteAsync(_meetingHistory.MeetingDate);
// Load all meeting histories to find previous/next
await LoadNavigationMeetings();
} }
} }
catch (TaskCanceledException) catch (TaskCanceledException)
@@ -180,6 +219,91 @@
} }
} }
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<MeetingHistoryDetailDialog>("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<MeetingHistoryDetailDialog>("Meeting History", parameters, options);
}
private async Task ConfirmDelete() private async Task ConfirmDelete()
{ {
if (_isDisposed || _isDeleting || _meetingHistory == null) return; if (_isDisposed || _isDeleting || _meetingHistory == null) return;
@@ -255,6 +379,36 @@
await LoadMeetingHistory(); 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<Team>(), // Not used when editing
["AbsentStudents"] = new List<Student>() // Not used when editing
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Medium,
FullWidth = true,
CloseButton = true
};
var dialog = await DialogService.ShowAsync<SaveMeetingHistoryDialog>("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<Student> GetAllStudentsFromTeams() private List<Student> GetAllStudentsFromTeams()
{ {
if (_meetingHistory == null) if (_meetingHistory == null)
@@ -4,6 +4,8 @@
@using Core.Services @using Core.Services
@using WebApp.Services @using WebApp.Services
@using WebApp.Components.Shared.Components @using WebApp.Components.Shared.Components
@using WebApp.Components.Features.Teams.Components
@using WebApp.Components.Features.Students.Components
@using WebApp.Models @using WebApp.Models
@inject ITeamMeetingHistoryService TeamMeetingHistoryService @inject ITeamMeetingHistoryService TeamMeetingHistoryService
@inject INotesService NotesService @inject INotesService NotesService
@@ -20,49 +22,32 @@
else else
{ {
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudText Typo="Typo.h6">Save Meeting History</MudText> <MudText Typo="Typo.h6">@(_isEditMode ? "Edit Meeting History" : "Save Meeting History")</MudText>
<MudDatePicker Label="Meeting Date" <MudDatePicker Label="Meeting Date"
Date="_meetingDate" Date="_meetingDate"
DateChanged="OnMeetingDateChanged" DateChanged="OnMeetingDateChanged"
Variant="Variant.Outlined" /> Variant="Variant.Outlined"
Disabled="@_isEditMode" />
<MudDivider /> <MudDivider />
<MudText Typo="Typo.subtitle1">Teams That Met</MudText>
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;"> <MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
<MudToggleGroup T="Team" <TeamToggleSelector Teams="@AllTeams"
SelectionMode="SelectionMode.MultiSelection" SelectedTeams="_selectedTeams"
Values="_selectedTeams" SelectedTeamsChanged="OnTeamsChanged"
ValuesChanged="OnTeamsChanged" Title="Teams That Met"
Vertical="true" ShowEventAttributes="false" />
CheckMark>
@foreach (var team in AllTeams.OrderByEventFormatFirst().ThenBy(e => e.ToString()))
{
<MudToggleItem Value="@team" Style="font-size: .75rem;">
@team.ToString()
</MudToggleItem>
}
</MudToggleGroup>
</MudPaper> </MudPaper>
<MudDivider /> <MudDivider />
<MudText Typo="Typo.subtitle1">Students Present</MudText>
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;"> <MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
<MudToggleGroup T="Student" <StudentToggleSelector Students="@AllStudents"
SelectionMode="SelectionMode.MultiSelection" SelectedStudents="_selectedStudents"
Values="_selectedStudents" SelectedStudentsChanged="OnStudentsChanged"
ValuesChanged="OnStudentsChanged" Title="Students Present"
Vertical="true" ShowFullName="true" />
CheckMark>
@foreach (var student in AllStudents.OrderBy(s => s.FirstName))
{
<MudToggleItem Value="@student" Style="font-size: .75rem;">
@student.FirstNameLastName
</MudToggleItem>
}
</MudToggleGroup>
</MudPaper> </MudPaper>
<MudDivider /> <MudDivider />
@@ -121,13 +106,17 @@
[Parameter] [Parameter]
public IEnumerable<Student> AllStudents { get; set; } = []; public IEnumerable<Student> AllStudents { get; set; } = [];
[Parameter]
public int? MeetingHistoryId { get; set; }
private DateTime? _meetingDate = DateTime.Today; private DateTime? _meetingDate = DateTime.Today;
private List<Team> _selectedTeams = []; private IEnumerable<Team> _selectedTeams = [];
private List<Student> _selectedStudents = []; private IEnumerable<Student> _selectedStudents = [];
private string _noteTitle = ""; private string _noteTitle = "";
private string _noteContent = ""; private string _noteContent = "";
private bool _isLoading = true; private bool _isLoading = true;
private bool _isSaving = false; private bool _isSaving = false;
private bool _isEditMode = false;
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false; private bool _isDisposed = false;
@@ -147,12 +136,41 @@
try try
{ {
// Initialize selected teams from scheduled teams // If editing, load existing meeting history
_selectedTeams = ScheduledTeams.ToList(); 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();
_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 ?? "";
}
}
}
else
{
// Initialize selected teams from scheduled teams
_selectedTeams = ScheduledTeams;
// Initialize selected students (all students except absent ones) // Initialize selected students (all students except absent ones)
var absentStudentIds = AbsentStudents.Select(s => s.Id).ToHashSet(); var absentStudentIds = AbsentStudents.Select(s => s.Id).ToHashSet();
_selectedStudents = AllStudents.Where(s => !absentStudentIds.Contains(s.Id)).ToList(); _selectedStudents = AllStudents.Where(s => !absentStudentIds.Contains(s.Id));
}
// Generate default note title if meeting date is set // Generate default note title if meeting date is set
if (_meetingDate.HasValue) if (_meetingDate.HasValue)
@@ -208,13 +226,13 @@
private void OnTeamsChanged(IEnumerable<Team> teams) private void OnTeamsChanged(IEnumerable<Team> teams)
{ {
_selectedTeams = teams.ToList(); _selectedTeams = teams;
StateHasChanged(); StateHasChanged();
} }
private void OnStudentsChanged(IEnumerable<Student> students) private void OnStudentsChanged(IEnumerable<Student> students)
{ {
_selectedStudents = students.ToList(); _selectedStudents = students;
StateHasChanged(); StateHasChanged();
} }
@@ -228,6 +246,21 @@
return; 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())
{
Snackbar.Add($"A meeting history already exists for {dateOnly:MM/dd/yyyy}. Only one meeting per day is allowed.", Severity.Warning);
return;
}
}
try try
{ {
_isSaving = true; _isSaving = true;
@@ -262,19 +295,52 @@
} }
} }
// Create meeting history // Create or update meeting history
var meetingHistory = new TeamMeetingHistory TeamMeetingHistory meetingHistory;
if (_isEditMode && MeetingHistoryId.HasValue)
{ {
MeetingDate = _meetingDate.Value, // Update existing meeting history
Teams = _selectedTeams, var existingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(MeetingHistoryId.Value);
Students = _selectedStudents if (existingHistory == null)
}; {
Snackbar.Add("Meeting history not found", Severity.Error);
_isSaving = false;
StateHasChanged();
return;
}
meetingHistory = existingHistory;
meetingHistory.MeetingDate = _meetingDate.Value;
meetingHistory.Teams = _selectedTeams.ToList();
meetingHistory.Students = _selectedStudents.ToList();
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); await TeamMeetingHistoryService.CreateMeetingHistoryAsync(meetingHistory);
if (!_isDisposed)
{
Snackbar.Add($"Meeting history saved for {_meetingDate.Value:MM/dd/yyyy}", Severity.Success);
}
}
if (!_isDisposed) if (!_isDisposed)
{ {
Snackbar.Add($"Meeting history saved for {_meetingDate.Value:MM/dd/yyyy}", Severity.Success);
MudDialog.Close(DialogResult.Ok(true)); MudDialog.Close(DialogResult.Ok(true));
} }
} }