6bc4c2e7f2
This commit introduces the TeamMeetingHistory entity, including its configuration and database migrations. A new ITeamMeetingHistoryService interface and its implementation, TeamMeetingHistoryService, are added to handle CRUD operations for meeting histories. Additionally, UI components such as History.razor, MeetingHistoryDetailDialog, and SaveMeetingHistoryDialog are created to facilitate viewing and saving meeting histories. The integration of INoteNamingService enhances note management for meeting records, improving overall functionality and user experience in the application.
318 lines
11 KiB
Plaintext
318 lines
11 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.Models
|
|
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
|
|
@inject INotesService NotesService
|
|
@inject INoteNamingService NoteNamingService
|
|
@inject ISnackbar Snackbar
|
|
@implements IAsyncDisposable
|
|
|
|
<MudDialog>
|
|
<DialogContent>
|
|
@if (_isLoading)
|
|
{
|
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
|
|
}
|
|
else
|
|
{
|
|
<MudStack Spacing="3">
|
|
<MudText Typo="Typo.h6">Save Meeting History</MudText>
|
|
|
|
<MudDatePicker Label="Meeting Date"
|
|
Date="_meetingDate"
|
|
DateChanged="OnMeetingDateChanged"
|
|
Variant="Variant.Outlined" />
|
|
|
|
<MudDivider />
|
|
|
|
<MudText Typo="Typo.subtitle1">Teams That Met</MudText>
|
|
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
|
|
<MudToggleGroup T="Team"
|
|
SelectionMode="SelectionMode.MultiSelection"
|
|
Values="_selectedTeams"
|
|
ValuesChanged="OnTeamsChanged"
|
|
Vertical="true"
|
|
CheckMark>
|
|
@foreach (var team in AllTeams.OrderByEventFormatFirst().ThenBy(e => e.ToString()))
|
|
{
|
|
<MudToggleItem Value="@team" Style="font-size: .75rem;">
|
|
@team.ToString()
|
|
</MudToggleItem>
|
|
}
|
|
</MudToggleGroup>
|
|
</MudPaper>
|
|
|
|
<MudDivider />
|
|
|
|
<MudText Typo="Typo.subtitle1">Students Present</MudText>
|
|
<MudPaper Elevation="1" Class="pa-2" Style="max-height: 300px; overflow-y: auto;">
|
|
<MudToggleGroup T="Student"
|
|
SelectionMode="SelectionMode.MultiSelection"
|
|
Values="_selectedStudents"
|
|
ValuesChanged="OnStudentsChanged"
|
|
Vertical="true"
|
|
CheckMark>
|
|
@foreach (var student in AllStudents.OrderBy(s => s.FirstName))
|
|
{
|
|
<MudToggleItem Value="@student" Style="font-size: .75rem;">
|
|
@student.FirstNameLastName
|
|
</MudToggleItem>
|
|
}
|
|
</MudToggleGroup>
|
|
</MudPaper>
|
|
|
|
<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; } = [];
|
|
|
|
private DateTime? _meetingDate = DateTime.Today;
|
|
private List<Team> _selectedTeams = [];
|
|
private List<Student> _selectedStudents = [];
|
|
private string _noteTitle = "";
|
|
private string _noteContent = "";
|
|
private bool _isLoading = true;
|
|
private bool _isSaving = 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
|
|
{
|
|
// Initialize selected teams from scheduled teams
|
|
_selectedTeams = ScheduledTeams.ToList();
|
|
|
|
// Initialize selected students (all students except absent ones)
|
|
var absentStudentIds = AbsentStudents.Select(s => s.Id).ToHashSet();
|
|
_selectedStudents = AllStudents.Where(s => !absentStudentIds.Contains(s.Id)).ToList();
|
|
|
|
// 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.ToList();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void OnStudentsChanged(IEnumerable<Student> students)
|
|
{
|
|
_selectedStudents = students.ToList();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task Save()
|
|
{
|
|
if (_isDisposed || _isSaving) return;
|
|
|
|
if (!_meetingDate.HasValue)
|
|
{
|
|
Snackbar.Add("Please select a meeting date", Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
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 meeting history
|
|
var meetingHistory = new TeamMeetingHistory
|
|
{
|
|
MeetingDate = _meetingDate.Value,
|
|
Teams = _selectedTeams,
|
|
Students = _selectedStudents
|
|
};
|
|
|
|
await TeamMeetingHistoryService.CreateMeetingHistoryAsync(meetingHistory);
|
|
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Meeting history saved for {_meetingDate.Value:MM/dd/yyyy}", 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 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;
|
|
}
|
|
}
|