Add TeamMeetingHistory entity, service, and UI components for meeting history management
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.
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
@page "/meeting-schedule/history"
|
||||
@attribute [Authorize]
|
||||
@using Core.Entities
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using WebApp.Services
|
||||
@inject ITeamMeetingHistoryService TeamMeetingHistoryService
|
||||
@inject AppDbContext Context
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IConfiguration Configuration
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<PageHeader Title="Team Meeting Schedule History">
|
||||
<ActionButtons>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Href="/meeting-schedule">
|
||||
Back to Schedule
|
||||
</MudButton>
|
||||
</ActionButtons>
|
||||
</PageHeader>
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-3 pa-md-6 mt-4">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-4" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="3">
|
||||
@if (_meetingHistories.Any())
|
||||
{
|
||||
<MudPaper Elevation="1" Class="pa-3" Style="overflow-x: auto;">
|
||||
<MudTable Items="_meetingHistories"
|
||||
Hover="true"
|
||||
Striped="true"
|
||||
Dense="true"
|
||||
Breakpoint="Breakpoint.Sm">
|
||||
<ColGroup>
|
||||
<col style="width: 100px;" />
|
||||
@foreach (var team in _allTeams)
|
||||
{
|
||||
<col style="width: 40px;" />
|
||||
}
|
||||
</ColGroup>
|
||||
<HeaderContent>
|
||||
<MudTh Style="padding: 4px 8px; border-right: 2px solid var(--mud-palette-divider);">
|
||||
<MudText Typo="Typo.caption" Style="writing-mode: vertical-rl; text-orientation: mixed;">
|
||||
Date
|
||||
</MudText>
|
||||
</MudTh>
|
||||
@{
|
||||
var teamIndex = 0;
|
||||
}
|
||||
@foreach (var team in _allTeams)
|
||||
{
|
||||
var isEven = teamIndex % 2 == 0;
|
||||
var bgColor = isEven ? "background-color: var(--mud-palette-background-grey);" : "";
|
||||
var headerStyle = $"padding: 4px 2px; border-right: 1px solid var(--mud-palette-divider); {bgColor}";
|
||||
<MudTh Style="@headerStyle">
|
||||
<MudText Typo="Typo.caption" Style="writing-mode: vertical-rl; text-orientation: mixed; transform: rotate(180deg);">
|
||||
@team.ToString()
|
||||
</MudText>
|
||||
</MudTh>
|
||||
teamIndex++;
|
||||
}
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd Style="padding: 4px 8px; border-right: 2px solid var(--mud-palette-divider);">
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => ViewMeetingDetails(context))"
|
||||
Style="text-transform: none; padding: 2px 4px;">
|
||||
@context.MeetingDate.ToString("MM/dd/yy")
|
||||
</MudButton>
|
||||
</MudTd>
|
||||
@{
|
||||
var rowTeamIndex = 0;
|
||||
}
|
||||
@foreach (var team in _allTeams)
|
||||
{
|
||||
var met = TeamMetOnDate(context, team);
|
||||
var isEven = rowTeamIndex % 2 == 0;
|
||||
var bgColor = isEven ? "background-color: var(--mud-palette-background-grey);" : "";
|
||||
var cellStyle = $"padding: 4px 2px; border-right: 1px solid var(--mud-palette-divider); {bgColor}";
|
||||
<MudTd Align="Align.Center" Style="@cellStyle">
|
||||
@if (met)
|
||||
{
|
||||
<MudText Typo="Typo.body1" Style="font-weight: bold;">×</MudText>
|
||||
}
|
||||
</MudTd>
|
||||
rowTeamIndex++;
|
||||
}
|
||||
</RowTemplate>
|
||||
<FooterContent>
|
||||
<MudTd Style="padding: 4px 8px; border-right: 2px solid var(--mud-palette-divider); font-weight: bold;">
|
||||
Times Met
|
||||
</MudTd>
|
||||
@{
|
||||
var footerTeamIndex = 0;
|
||||
}
|
||||
@foreach (var team in _allTeams)
|
||||
{
|
||||
var timesMet = GetTimesMetForTeam(team);
|
||||
var isEven = footerTeamIndex % 2 == 0;
|
||||
var bgColor = isEven ? "background-color: var(--mud-palette-background-grey);" : "";
|
||||
var footerCellStyle = $"padding: 4px 2px; border-right: 1px solid var(--mud-palette-divider); {bgColor}";
|
||||
<MudTd Align="Align.Center" Style="@footerCellStyle">
|
||||
<MudText Typo="Typo.body2" Style="font-weight: bold;">@timesMet</MudText>
|
||||
</MudTd>
|
||||
footerTeamIndex++;
|
||||
}
|
||||
</FooterContent>
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudAlert Severity="Severity.Info">No meeting history found. Save a meeting schedule to get started.</MudAlert>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private List<TeamMeetingHistory> _meetingHistories = [];
|
||||
private List<Team> _allTeams = [];
|
||||
private Dictionary<int, int> _timesMetDict = new();
|
||||
private bool _isLoading = true;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadData();
|
||||
}
|
||||
|
||||
private async Task LoadData()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Load all teams for columns
|
||||
_allTeams = await Context.Teams
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Event)
|
||||
.OrderBy(t => t.Event.Name)
|
||||
.ThenBy(t => t.Identifier)
|
||||
.ToListAsync();
|
||||
|
||||
// Load meeting histories
|
||||
await RefreshMeetingHistories();
|
||||
|
||||
// Calculate times met
|
||||
CalculateTimesMet();
|
||||
}
|
||||
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 RefreshMeetingHistories()
|
||||
{
|
||||
_meetingHistories = (await TeamMeetingHistoryService.GetMeetingHistoriesAsync()).ToList();
|
||||
|
||||
// Extract all unique teams from meeting histories and merge with all teams
|
||||
var teamsInHistory = _meetingHistories
|
||||
.SelectMany(mh => mh.Teams)
|
||||
.DistinctBy(t => t.Id)
|
||||
.ToList();
|
||||
|
||||
// Merge with all teams, ensuring all teams are included
|
||||
var allTeamIds = _allTeams.Select(t => t.Id).ToHashSet();
|
||||
var newTeams = teamsInHistory.Where(t => !allTeamIds.Contains(t.Id)).ToList();
|
||||
_allTeams = _allTeams.Concat(newTeams).OrderBy(t => t.Event?.Name ?? "").ThenBy(t => t.Identifier).ToList();
|
||||
}
|
||||
|
||||
private void CalculateTimesMet()
|
||||
{
|
||||
_timesMetDict.Clear();
|
||||
|
||||
foreach (var team in _allTeams)
|
||||
{
|
||||
_timesMetDict[team.Id] = 0;
|
||||
}
|
||||
|
||||
foreach (var history in _meetingHistories)
|
||||
{
|
||||
var teamIds = history.Teams.Select(t => t.Id).ToHashSet();
|
||||
foreach (var teamId in teamIds)
|
||||
{
|
||||
if (_timesMetDict.ContainsKey(teamId))
|
||||
{
|
||||
_timesMetDict[teamId]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int GetTimesMetForTeam(Team team)
|
||||
{
|
||||
return _timesMetDict.GetValueOrDefault(team.Id, 0);
|
||||
}
|
||||
|
||||
private bool TeamMetOnDate(TeamMeetingHistory history, Team team)
|
||||
{
|
||||
return history.Teams.Any(t => t.Id == team.Id);
|
||||
}
|
||||
|
||||
private async Task ViewMeetingDetails(TeamMeetingHistory history)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["MeetingHistoryId"] = history.Id
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Large,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<MeetingHistoryDetailDialog>("Meeting Details", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
if (!result.Canceled)
|
||||
{
|
||||
// Refresh data if meeting was updated or deleted
|
||||
await RefreshMeetingHistories();
|
||||
CalculateTimesMet();
|
||||
if (!_isDisposed)
|
||||
{
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
await ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,34 @@
|
||||
@using Core.Calculation
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using WebApp.Components.Features.MeetingSchedule
|
||||
@using Core.Utility
|
||||
@inject IConfiguration Configuration
|
||||
@inject AppDbContext Context
|
||||
@inject ClipboardService ClipboardService
|
||||
@inject LocalStorageService LocalStorage
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageHeader Title="@($"{Configuration["ChapterSettings:Shortname"]} TSA Schedule {Configuration["ChapterSettings:CompetitionYear"]}")">
|
||||
<ActionButtons>
|
||||
<MudTooltip Text="View meeting history">
|
||||
<MudButton StartIcon="@Icons.Material.Filled.History"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Default"
|
||||
Href="/meeting-schedule/history">
|
||||
View History
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Save current schedule as meeting history">
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Save"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
OnClick="OpenSaveHistoryDialog"
|
||||
Disabled="@(!_scheduledTeams.Any())">
|
||||
Save as History
|
||||
</MudButton>
|
||||
</MudTooltip>
|
||||
<PageNoteButton PageIdentifier="Meeting Schedule" />
|
||||
</ActionButtons>
|
||||
</PageHeader>
|
||||
@@ -671,6 +691,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenSaveHistoryDialog()
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
["ScheduledTeams"] = _scheduledTeams,
|
||||
["AbsentStudents"] = _absentStudents,
|
||||
["AllTeams"] = _teams,
|
||||
["AllStudents"] = _students
|
||||
};
|
||||
|
||||
var options = new DialogOptions
|
||||
{
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
FullWidth = true,
|
||||
CloseButton = true
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<SaveMeetingHistoryDialog>("Save Meeting History", parameters, options);
|
||||
var result = await dialog.Result;
|
||||
|
||||
// Note: Success message is already shown in the dialog, no need to show another here
|
||||
}
|
||||
|
||||
private void AppendScheduledTeams(StringBuilder sb, TeamScheduleTimeSlot timeslot)
|
||||
{
|
||||
var timeSlotIndex = GetTimeSlotIndex(timeslot.Name);
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
@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
|
||||
@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">
|
||||
<MudText Typo="Typo.h6">Meeting Details</MudText>
|
||||
|
||||
<MudPaper Elevation="1" Class="pa-3">
|
||||
<MudStack Spacing="2">
|
||||
<MudText Typo="Typo.subtitle2">
|
||||
<strong>Date:</strong> @_meetingHistory.MeetingDate.ToString("MM/dd/yyyy")
|
||||
</MudText>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
|
||||
<MudDivider />
|
||||
|
||||
<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 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 CancellationTokenSource? _cancellationTokenSource;
|
||||
private bool _isDisposed = false;
|
||||
private bool IsActionDisabled => _isLoading || _isDeleting;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 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 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 void Close()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
MudDialog.Close();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (!_isDisposed)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource?.Dispose();
|
||||
_cancellationTokenSource = null;
|
||||
}
|
||||
await ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
@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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user