9ab241ed77
This commit modifies the MeetingSchedule component to change the page header title to "Meeting Scheduler Planner" for better clarity. Additionally, it refines the MeetingHistoryDetailDialog by removing unnecessary UI elements to streamline the display of meeting history details. In the Printout component, null checks are added to ensure safe access to LevelOfEffort properties, enhancing data integrity. The Home component's title is also updated to "Meeting Schedule Planner" for consistency across the application. These changes collectively enhance the user experience and maintain data accuracy in the meeting scheduling feature.
503 lines
18 KiB
Plaintext
503 lines
18 KiB
Plaintext
@namespace WebApp.Components.Features.MeetingSchedule
|
|
@using Core.Entities
|
|
@using Core.Services
|
|
@using Core.Utility
|
|
@using WebApp.Services
|
|
@using WebApp.Components.Shared.Components
|
|
@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;
|
|
}
|
|
}
|