From 15d7edec8f67c96da8f1b2f1e0c728dde9017ec6 Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Mon, 26 Jan 2026 23:31:59 -0500 Subject: [PATCH] Add Team Meeting History functionality with dialog and badge components This commit introduces a new feature to display meeting history for teams. It adds a `TeamMeetingHistoryDialog` component that shows the meeting history in a dialog format, including a loading state and error handling. Additionally, a `TeamMeetingHistoryBadge` component is created to display the count of meetings for each team, enhancing the user interface with tooltips for better interaction. The `Details` and `Index` components are updated to integrate these new features, allowing users to view meeting history directly from the team details and index pages. These changes improve the overall functionality and user experience in managing team meetings. --- .../Components/TeamMeetingHistoryBadge.razor | 108 +++++++++++++++ .../TeamMeetingToggleSelector.razor | 20 +-- .../Components/Features/Teams/Details.razor | 29 ++++ WebApp/Components/Features/Teams/Index.razor | 5 +- .../Teams/TeamMeetingHistoryDialog.razor | 127 ++++++++++++++++++ WebApp/Services/ITeamMeetingHistoryService.cs | 15 +++ WebApp/Services/TeamMeetingHistoryService.cs | 21 +++ 7 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 WebApp/Components/Features/Teams/Components/TeamMeetingHistoryBadge.razor create mode 100644 WebApp/Components/Features/Teams/TeamMeetingHistoryDialog.razor diff --git a/WebApp/Components/Features/Teams/Components/TeamMeetingHistoryBadge.razor b/WebApp/Components/Features/Teams/Components/TeamMeetingHistoryBadge.razor new file mode 100644 index 0000000..16a55c2 --- /dev/null +++ b/WebApp/Components/Features/Teams/Components/TeamMeetingHistoryBadge.razor @@ -0,0 +1,108 @@ +@namespace WebApp.Components.Features.Teams.Components +@using Core.Entities +@using WebApp.Services +@using MudBlazor +@inject ITeamMeetingHistoryService TeamMeetingHistoryService +@inject IDialogService DialogService +@implements IAsyncDisposable + +@if (_meetingCount.HasValue && _meetingCount.Value > 0) +{ + + + + + +} + +@code { + [Parameter] + public int TeamId { get; set; } + + [Parameter] + public string TeamName { get; set; } = string.Empty; + + private int? _meetingCount; + private CancellationTokenSource? _cancellationTokenSource; + private bool _isDisposed = false; + + protected override void OnInitialized() + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + protected override async Task OnInitializedAsync() + { + await LoadMeetingCount(); + } + + private async Task LoadMeetingCount() + { + if (_isDisposed) return; + + try + { + var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None; + _meetingCount = await TeamMeetingHistoryService.GetMeetingHistoryCountForTeamAsync(TeamId); + } + catch (TaskCanceledException) + { + // Component was disposed, ignore + } + catch (JSDisconnectedException) + { + // JS connection lost, ignore + } + catch (Exception) + { + // Ignore errors - just don't show the badge + _meetingCount = null; + } + finally + { + if (!_isDisposed) + { + StateHasChanged(); + } + } + } + + private async Task OpenMeetingHistoryDialog() + { + if (_isDisposed) return; + + var parameters = new DialogParameters + { + ["TeamId"] = TeamId, + ["TeamName"] = TeamName + }; + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + CloseButton = true, + MaxWidth = MaxWidth.Medium, + FullWidth = true + }; + + await DialogService.ShowAsync($"{TeamName} Meeting History", parameters, options); + } + + public async ValueTask DisposeAsync() + { + if (!_isDisposed) + { + _isDisposed = true; + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + } + await ValueTask.CompletedTask; + } +} diff --git a/WebApp/Components/Features/Teams/Components/TeamMeetingToggleSelector.razor b/WebApp/Components/Features/Teams/Components/TeamMeetingToggleSelector.razor index 6393b28..f28d56c 100644 --- a/WebApp/Components/Features/Teams/Components/TeamMeetingToggleSelector.razor +++ b/WebApp/Components/Features/Teams/Components/TeamMeetingToggleSelector.razor @@ -1,5 +1,6 @@ @using WebApp.Models @using Core.Utility +@using WebApp.Components.Features.Teams.Components @if (Title != null) { @@ -28,14 +29,17 @@ @if (IsSelected(team)) { var isExtended = IsExtended(team); - - - + + + + + + } @if (ShowEventAttributes) { diff --git a/WebApp/Components/Features/Teams/Details.razor b/WebApp/Components/Features/Teams/Details.razor index 1a996b5..cf71cd5 100644 --- a/WebApp/Components/Features/Teams/Details.razor +++ b/WebApp/Components/Features/Teams/Details.razor @@ -3,9 +3,11 @@ @using Microsoft.EntityFrameworkCore @using WebApp.Components.Shared.Components @using WebApp.Components.Features.Teams.Components +@using MudBlazor @inject AppDbContext Context @inject NavigationManager NavigationManager @inject IJSRuntime JSRuntime +@inject IDialogService DialogService @if (Team is null) { @@ -29,6 +31,11 @@ Href="@($"/teams/edit?id={Team.Id}&returnUrl={ReturnUrl ?? "/teams"}")" Variant="Variant.Outlined">Edit + + Meeting History + @@ -86,4 +93,26 @@ { await JSRuntime.InvokeVoidAsync("window.print"); } + + private async Task ShowMeetingHistory() + { + if (Team == null) return; + + var teamName = Team.ToString(); + var parameters = new DialogParameters + { + ["TeamId"] = Team.Id, + ["TeamName"] = teamName + }; + + var options = new DialogOptions + { + CloseOnEscapeKey = true, + CloseButton = true, + MaxWidth = MaxWidth.Medium, + FullWidth = true + }; + + await DialogService.ShowAsync($"{teamName} Meeting History", parameters, options); + } } diff --git a/WebApp/Components/Features/Teams/Index.razor b/WebApp/Components/Features/Teams/Index.razor index 52dad5a..a4c0ae8 100644 --- a/WebApp/Components/Features/Teams/Index.razor +++ b/WebApp/Components/Features/Teams/Index.razor @@ -47,11 +47,12 @@ + Underline="Underline.Hover" + Color="Color.Primary"> @context.Item.ToString() + diff --git a/WebApp/Components/Features/Teams/TeamMeetingHistoryDialog.razor b/WebApp/Components/Features/Teams/TeamMeetingHistoryDialog.razor new file mode 100644 index 0000000..ae38848 --- /dev/null +++ b/WebApp/Components/Features/Teams/TeamMeetingHistoryDialog.razor @@ -0,0 +1,127 @@ +@namespace WebApp.Components.Features.Teams +@using Core.Entities +@using WebApp.Services +@using Microsoft.AspNetCore.Components +@inject ITeamMeetingHistoryService TeamMeetingHistoryService +@implements IAsyncDisposable + + + + @TeamName Meeting History + + + @if (_isLoading) + { + + } + else if (!_meetingHistories.Any()) + { + No meeting history found for this team. + } + else + { + + + Date + Status + + + @{ + var teamWasInMeeting = context.Teams.Any(t => t.Id == TeamId); + } + @context.MeetingDate.ToString("MM/dd/yyyy") + + + @(teamWasInMeeting ? "Team Met" : "Team Not Scheduled") + + + + + } + + + + Close + + + +@code { + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public int TeamId { get; set; } + + [Parameter] + public string TeamName { get; set; } = string.Empty; + + private List _meetingHistories = []; + private bool _isLoading = true; + private CancellationTokenSource? _cancellationTokenSource; + private bool _isDisposed = false; + + protected override void OnInitialized() + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + protected override async Task OnInitializedAsync() + { + await LoadMeetingHistories(); + } + + private async Task LoadMeetingHistories() + { + if (_isDisposed) return; + + try + { + var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None; + var histories = await TeamMeetingHistoryService.GetMeetingHistoriesForTeamAsync(TeamId); + _meetingHistories = histories.ToList(); + } + catch (TaskCanceledException) + { + // Component was disposed, ignore + } + catch (JSDisconnectedException) + { + // JS connection lost, ignore + } + catch (Exception ex) + { + if (!_isDisposed) + { + System.Diagnostics.Debug.WriteLine($"Error loading meeting histories: {ex.Message}"); + } + } + finally + { + if (!_isDisposed) + { + _isLoading = false; + StateHasChanged(); + } + } + } + + 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; + } +} diff --git a/WebApp/Services/ITeamMeetingHistoryService.cs b/WebApp/Services/ITeamMeetingHistoryService.cs index 3d43044..d067e00 100644 --- a/WebApp/Services/ITeamMeetingHistoryService.cs +++ b/WebApp/Services/ITeamMeetingHistoryService.cs @@ -47,4 +47,19 @@ public interface ITeamMeetingHistoryService /// The date of the meeting /// The note if found, null otherwise Task GetMeetingNoteAsync(DateTime meetingDate); + + /// + /// Gets all meeting histories for a specific team, ordered by date descending. + /// + /// The ID of the team + /// Meeting histories for the team, ordered by date descending + Task> GetMeetingHistoriesForTeamAsync(int teamId); + + /// + /// Gets the count of meeting histories for a specific team. + /// This is more efficient than loading all histories just to count them. + /// + /// The ID of the team + /// The number of meeting histories for the team + Task GetMeetingHistoryCountForTeamAsync(int teamId); } diff --git a/WebApp/Services/TeamMeetingHistoryService.cs b/WebApp/Services/TeamMeetingHistoryService.cs index 704afe7..679e0c6 100644 --- a/WebApp/Services/TeamMeetingHistoryService.cs +++ b/WebApp/Services/TeamMeetingHistoryService.cs @@ -202,4 +202,25 @@ public class TeamMeetingHistoryService : ITeamMeetingHistoryService .Distinct() .ToListAsync(); } + + public async Task> GetMeetingHistoriesForTeamAsync(int teamId) + { + return await _context.TeamMeetingHistories + .AsNoTracking() + .Include(tmh => tmh.Teams) + .ThenInclude(t => t.Event) + .Include(tmh => tmh.Students) + .Where(tmh => tmh.Teams.Any(t => t.Id == teamId)) + .OrderByDescending(tmh => tmh.MeetingDate) + .ThenByDescending(tmh => tmh.Id) + .ToListAsync(); + } + + public async Task GetMeetingHistoryCountForTeamAsync(int teamId) + { + return await _context.TeamMeetingHistories + .AsNoTracking() + .Where(tmh => tmh.Teams.Any(t => t.Id == teamId)) + .CountAsync(); + } }