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.
455 lines
18 KiB
Plaintext
455 lines
18 KiB
Plaintext
@namespace WebApp.Components.Features.MeetingSchedule
|
|
@using Core.Entities
|
|
@using Core.Calculation
|
|
@using Core.Services
|
|
@using WebApp.Services
|
|
@using WebApp.Components.Shared.Components
|
|
@using WebApp.Components.Features.Teams.Components
|
|
@using WebApp.Components.Features.Students.Components
|
|
@using WebApp.Models
|
|
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
|
|
@inject INotesService NotesService
|
|
@inject INoteNamingService NoteNamingService
|
|
@inject ISnackbar Snackbar
|
|
@inject IDialogService DialogService
|
|
@implements IAsyncDisposable
|
|
|
|
<MudDialog>
|
|
<DialogContent>
|
|
@if (_isLoading)
|
|
{
|
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
|
|
}
|
|
else
|
|
{
|
|
<MudStack Spacing="3">
|
|
<MudText Typo="Typo.h6">@(_isEditMode ? "Edit Meeting History" : "Save Meeting History")</MudText>
|
|
|
|
<MudDatePicker Label="Meeting Date"
|
|
Date="_meetingDate"
|
|
DateChanged="OnMeetingDateChanged"
|
|
Variant="Variant.Outlined"
|
|
Disabled="@_isEditMode" />
|
|
|
|
<MudDivider />
|
|
|
|
<MudGrid>
|
|
<MudItem xs="12" md="6">
|
|
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
|
|
<TeamToggleSelector Teams="@AllTeams"
|
|
SelectedTeams="_selectedTeams"
|
|
SelectedTeamsChanged="OnTeamsChanged"
|
|
Title="Teams That Met"
|
|
ShowEventAttributes="false" />
|
|
</MudPaper>
|
|
</MudItem>
|
|
<MudItem xs="12" md="6">
|
|
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
|
|
<StudentToggleSelector Students="@AllStudents"
|
|
SelectedStudents="_selectedStudents"
|
|
SelectedStudentsChanged="OnStudentsChanged"
|
|
Title="Students Present"
|
|
ShowFullName="true" />
|
|
</MudPaper>
|
|
</MudItem>
|
|
</MudGrid>
|
|
|
|
<MudDivider />
|
|
|
|
<MudExpansionPanels MultiExpansion="false">
|
|
<MudExpansionPanel Text="Meeting Notes (Optional)">
|
|
<MudStack Spacing="2">
|
|
<MudTextField T="string"
|
|
Label="Note Title"
|
|
@bind-Value="_noteTitle"
|
|
Variant="Variant.Outlined"
|
|
ReadOnly="true"
|
|
HelperText="Title is automatically generated based on meeting date" />
|
|
<MudTextField T="string"
|
|
Label="Note Content"
|
|
@bind-Value="_noteContent"
|
|
Variant="Variant.Outlined"
|
|
Lines="5"
|
|
Placeholder="Enter meeting notes..."
|
|
HelperText="Optional markdown content for meeting notes" />
|
|
</MudStack>
|
|
</MudExpansionPanel>
|
|
</MudExpansionPanels>
|
|
</MudStack>
|
|
}
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<MudButton OnClick="Cancel" Disabled="@_isSaving">Cancel</MudButton>
|
|
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="Save" Disabled="@IsSaveDisabled">
|
|
@if (_isSaving)
|
|
{
|
|
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
|
<span>Saving...</span>
|
|
}
|
|
else
|
|
{
|
|
<span>Save</span>
|
|
}
|
|
</MudButton>
|
|
</DialogActions>
|
|
</MudDialog>
|
|
|
|
@code {
|
|
[CascadingParameter]
|
|
IMudDialogInstance MudDialog { get; set; } = null!;
|
|
|
|
[Parameter]
|
|
public IEnumerable<Team> ScheduledTeams { get; set; } = [];
|
|
|
|
[Parameter]
|
|
public IEnumerable<Student> AbsentStudents { get; set; } = [];
|
|
|
|
[Parameter]
|
|
public IEnumerable<Team> AllTeams { get; set; } = [];
|
|
|
|
[Parameter]
|
|
public IEnumerable<Student> AllStudents { get; set; } = [];
|
|
|
|
[Parameter]
|
|
public int? MeetingHistoryId { get; set; }
|
|
|
|
private DateTime? _meetingDate = DateTime.Today;
|
|
private IEnumerable<Team> _selectedTeams = [];
|
|
private IEnumerable<Student> _selectedStudents = [];
|
|
private string _noteTitle = "";
|
|
private string _noteContent = "";
|
|
private bool _isLoading = true;
|
|
private bool _isSaving = false;
|
|
private bool _isEditMode = false;
|
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
private bool _isDisposed = false;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
}
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadInitialData();
|
|
}
|
|
|
|
private async Task LoadInitialData()
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
try
|
|
{
|
|
// If editing, load existing meeting history
|
|
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();
|
|
var matchedTeams = AllTeams.Where(t => selectedTeamIds.Contains(t.Id)).ToList();
|
|
|
|
// If we couldn't match all teams from AllTeams, use the teams from existing history
|
|
// This can happen if AllTeams is empty or doesn't contain all the teams
|
|
if (matchedTeams.Count != existingHistory.Teams.Count && AllTeams.Any())
|
|
{
|
|
// Try to match what we can, but log a warning
|
|
System.Diagnostics.Debug.WriteLine($"Warning: Could not match all teams. Expected {existingHistory.Teams.Count}, matched {matchedTeams.Count}");
|
|
}
|
|
_selectedTeams = matchedTeams.Any() ? matchedTeams : existingHistory.Teams;
|
|
|
|
// Match students by ID to ensure reference equality with MudToggleGroup
|
|
var selectedStudentIds = existingHistory.Students.Select(s => s.Id).ToHashSet();
|
|
var matchedStudents = AllStudents.Where(s => selectedStudentIds.Contains(s.Id)).ToList();
|
|
|
|
// If we couldn't match all students from AllStudents, use the students from existing history
|
|
if (matchedStudents.Count != existingHistory.Students.Count && AllStudents.Any())
|
|
{
|
|
// Try to match what we can, but log a warning
|
|
System.Diagnostics.Debug.WriteLine($"Warning: Could not match all students. Expected {existingHistory.Students.Count}, matched {matchedStudents.Count}");
|
|
}
|
|
_selectedStudents = matchedStudents.Any() ? matchedStudents : existingHistory.Students;
|
|
|
|
// 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)
|
|
var absentStudentIds = AbsentStudents.Select(s => s.Id).ToHashSet();
|
|
_selectedStudents = AllStudents.Where(s => !absentStudentIds.Contains(s.Id));
|
|
}
|
|
|
|
// Generate default note title if meeting date is set
|
|
if (_meetingDate.HasValue)
|
|
{
|
|
_noteTitle = NoteNamingService.GetMeetingNoteTitle(_meetingDate.Value);
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
// Component was disposed, ignore
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// JS connection lost, ignore
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Error loading data: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
_isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool IsSaveDisabled => _isSaving || _isLoading;
|
|
|
|
protected override async Task OnParametersSetAsync()
|
|
{
|
|
if (_meetingDate.HasValue && string.IsNullOrEmpty(_noteTitle))
|
|
{
|
|
_noteTitle = NoteNamingService.GetMeetingNoteTitle(_meetingDate.Value);
|
|
}
|
|
await base.OnParametersSetAsync();
|
|
}
|
|
|
|
private async Task OnMeetingDateChanged(DateTime? date)
|
|
{
|
|
_meetingDate = date;
|
|
if (date.HasValue)
|
|
{
|
|
_noteTitle = NoteNamingService.GetMeetingNoteTitle(date.Value);
|
|
}
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private void OnTeamsChanged(IEnumerable<Team> teams)
|
|
{
|
|
_selectedTeams = teams;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void OnStudentsChanged(IEnumerable<Student> students)
|
|
{
|
|
_selectedStudents = students;
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task Save()
|
|
{
|
|
if (_isDisposed || _isSaving) return;
|
|
|
|
if (!_meetingDate.HasValue)
|
|
{
|
|
Snackbar.Add("Please select a meeting date", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
// Validate that we have at least one team selected
|
|
if (!_selectedTeams.Any())
|
|
{
|
|
Snackbar.Add("Please select at least one team", Severity.Warning);
|
|
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())
|
|
{
|
|
// Show confirmation dialog
|
|
var confirmResult = await DialogService.ShowMessageBox(
|
|
"Overwrite Meeting?",
|
|
"A meeting already exists for this date. Overwrite it?",
|
|
yesText: "Overwrite",
|
|
cancelText: "Cancel");
|
|
|
|
if (confirmResult != true)
|
|
{
|
|
return; // User cancelled
|
|
}
|
|
|
|
// User confirmed, switch to edit mode
|
|
var existingMeeting = existingMeetings.First();
|
|
_isEditMode = true;
|
|
MeetingHistoryId = existingMeeting.Id;
|
|
|
|
// Load existing meeting data
|
|
var existingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(existingMeeting.Id);
|
|
if (existingHistory != null)
|
|
{
|
|
// 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 ?? "";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
_isSaving = true;
|
|
StateHasChanged();
|
|
|
|
// Create or update note if note content is provided
|
|
Note? note = null;
|
|
if (!string.IsNullOrWhiteSpace(_noteContent))
|
|
{
|
|
// Use the naming service to get the meeting note title
|
|
var noteTitle = NoteNamingService.GetMeetingNoteTitle(_meetingDate.Value);
|
|
|
|
// Check if note already exists
|
|
var existingNote = await NotesService.GetNotesAsync(includeDeleted: false);
|
|
note = existingNote.FirstOrDefault(n => n.Title == noteTitle);
|
|
|
|
if (note != null)
|
|
{
|
|
// Update existing note
|
|
note.Content = _noteContent;
|
|
note = await NotesService.UpdateNoteAsync(note);
|
|
}
|
|
else
|
|
{
|
|
// Create new note
|
|
note = new Note
|
|
{
|
|
Title = noteTitle,
|
|
Content = _noteContent
|
|
};
|
|
note = await NotesService.CreateNoteAsync(note);
|
|
}
|
|
}
|
|
|
|
// Create or update meeting history
|
|
TeamMeetingHistory meetingHistory;
|
|
if (_isEditMode && MeetingHistoryId.HasValue)
|
|
{
|
|
// Update existing meeting history
|
|
var existingHistory = await TeamMeetingHistoryService.GetMeetingHistoryAsync(MeetingHistoryId.Value);
|
|
if (existingHistory == null)
|
|
{
|
|
Snackbar.Add("Meeting history not found", Severity.Error);
|
|
_isSaving = false;
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
// Ensure we have teams and students - use existing if selected lists are empty (fallback)
|
|
var teamsToSave = _selectedTeams.Any() ? _selectedTeams.ToList() : existingHistory.Teams.ToList();
|
|
var studentsToSave = _selectedStudents.Any() ? _selectedStudents.ToList() : existingHistory.Students.ToList();
|
|
|
|
// Create a new meeting history object with the updated data
|
|
// Use IDs to ensure we're working with the correct entities
|
|
meetingHistory = new TeamMeetingHistory
|
|
{
|
|
Id = existingHistory.Id,
|
|
MeetingDate = _meetingDate.Value,
|
|
Teams = teamsToSave,
|
|
Students = studentsToSave
|
|
};
|
|
|
|
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);
|
|
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Meeting history saved for {_meetingDate.Value:MM/dd/yyyy}", Severity.Success);
|
|
}
|
|
}
|
|
|
|
if (!_isDisposed)
|
|
{
|
|
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 saving meeting history: {ex.Message}", Severity.Error);
|
|
_isSaving = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Cancel()
|
|
{
|
|
if (_isDisposed) return;
|
|
MudDialog.Cancel();
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
_isDisposed = true;
|
|
_cancellationTokenSource?.Cancel();
|
|
_cancellationTokenSource?.Dispose();
|
|
_cancellationTokenSource = null;
|
|
}
|
|
await ValueTask.CompletedTask;
|
|
}
|
|
}
|