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(); + } }