675f04afec
This commit introduces the CalendarService, which provides methods for retrieving all calendar items and upcoming events. The service is integrated into various components, including the Calendar and Home pages, enhancing the calendar functionality by allowing users to view upcoming events and meeting histories. Additionally, the Index.razor component is updated to parse query parameters for date selection, improving user experience. The layout of the MeetingHistoryDetailDialog and SaveMeetingHistoryDialog components is also refined for better organization and responsiveness. These changes collectively enhance the calendar feature and improve the overall user interface in managing events and meetings.
502 lines
18 KiB
Plaintext
502 lines
18 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>
|
|
|
|
<MudGrid>
|
|
<MudItem xs="12" md="6">
|
|
<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>
|
|
</MudItem>
|
|
<MudItem xs="12" md="6">
|
|
<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>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
@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;
|
|
}
|
|
}
|