840a8edbf1
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.
499 lines
17 KiB
Plaintext
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;
|
|
}
|
|
}
|