Files
chapter-organizer/WebApp/Components/Features/MeetingSchedule/MeetingHistoryDetailDialog.razor
T
poprhythm 840a8edbf1 Enhance Calendar component with improved event tooltip functionality and styling
This commit updates the Calendar component to include a new method for generating tooltips for both events and meetings, enhancing user interaction by providing contextual information. Additionally, CSS styles are introduced for calendar event items, improving their appearance and hover effects. These changes contribute to a more informative and visually appealing calendar experience, aligning with the ongoing efforts to refine the user interface.
2026-01-26 21:31:18 -05:00

499 lines
17 KiB
Plaintext

@namespace WebApp.Components.Features.MeetingSchedule
@using Core.Services
@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
<MudDialog>
<DialogContent>
@if (_isLoading)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
}
else if (_meetingHistory == null)
{
<MudAlert Severity="Severity.Error">Meeting history not found.</MudAlert>
}
else
{
<MudStack Spacing="3">
@* 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>
<MudText Typo="Typo.subtitle1">Teams That Met (@_meetingHistory.Teams.Count)</MudText>
<MudPaper Elevation="1" Class="pa-2">
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
@foreach (var team in _meetingHistory.Teams.OrderByEventFormatFirst().ThenBy(e => e.ToString()))
{
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="@AppIcons.TeamChipVariant()" Class="mx-1 my-1">@team.ToString()</MudChip>
}
</MudStack>
</MudPaper>
<MudDivider />
<MudText Typo="Typo.subtitle1">Students (@GetAllStudentsFromTeams().Count)</MudText>
<MudPaper Elevation="1" Class="pa-2">
<MudStack Row="true" Spacing="1" Wrap="Wrap.Wrap">
@{
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);
<MudChip T="string"
Size="Size.Small"
Color="Color.Default"
Variant="@AppIcons.StudentChipVariant()"
Class="mx-1 my-1"
Style="@(!isPresent ? "opacity: 0.5;" : "")">
@student.FirstNameLastName
</MudChip>
}
</MudStack>
</MudPaper>
@if (_meetingNote != null)
{
<MudDivider />
<MudText Typo="Typo.subtitle1">Meeting Notes</MudText>
<MudPaper Elevation="1" Class="pa-3">
<MudText Typo="Typo.body2"><strong>@_meetingNote.Title</strong></MudText>
@if (!string.IsNullOrWhiteSpace(_meetingNote.Content))
{
<MudText Typo="Typo.body2" Class="mt-2">
@((MarkupString)MarkdownHelper.ToHtml(_meetingNote.Content))
</MudText>
}
<MudButton Variant="Variant.Text"
Size="Size.Small"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Edit"
OnClick="ViewNote"
Class="mt-2">
Edit Note
</MudButton>
</MudPaper>
}
</MudStack>
}
</DialogContent>
<DialogActions>
@if (_meetingHistory != null)
{
<MudButton Variant="Variant.Text"
Color="Color.Error"
OnClick="ConfirmDelete"
Disabled="@IsActionDisabled">
Delete
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Text"
Color="Color.Secondary"
OnClick="LoadIntoPlanner"
Disabled="@IsActionDisabled"
StartIcon="@Icons.Material.Filled.Upload">
Load into Planner
</MudButton>
<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>
}
else
{
<MudSpacer />
<MudButton OnClick="Close">Close</MudButton>
}
</DialogActions>
</MudDialog>
@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<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()
{
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<NoteViewDialog>("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<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()
{
if (_meetingHistory == null)
return [];
var allStudents = new List<Student>();
var studentIds = new HashSet<int>();
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;
}
}