Files
chapter-organizer/WebApp/Components/Features/MeetingSchedule/History.razor
T
poprhythm 6bc4c2e7f2 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.
2026-01-19 22:02:59 -05:00

283 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@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;
}
}