Files
chapter-organizer/WebApp/Components/Features/MeetingSchedule/History.razor
T
poprhythm a503655f97 Enhance MeetingSchedule and MeetingHistoryDetailDialog with load into planner functionality
This commit adds a new feature to the MeetingSchedule and MeetingHistoryDetailDialog components, allowing users to load meeting details into the planner. The LoadMeetingIntoPlanner method is implemented in both components, which retrieves team and student data, calculates absent students, and saves the state to localStorage before navigating to the planner. Additionally, a confirmation dialog is introduced in the SaveMeetingHistoryDialog to handle overwriting existing meeting histories, improving user experience and data management in the meeting scheduling feature.
2026-01-25 18:58:02 -05:00

350 lines
14 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
@inject IMeetingScheduleDataService DataService
@inject IMeetingScheduleStateService StateService
@inject NavigationManager NavigationManager
@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;" />
<col style="width: 50px;" />
@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>
<MudTh Style="padding: 4px 8px; border-right: 2px solid var(--mud-palette-divider);">
<MudText Typo="Typo.caption">Actions</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>
<MudTd Style="padding: 4px 8px; border-right: 2px solid var(--mud-palette-divider);">
<MudTooltip Text="Load into Planner">
<MudIconButton Icon="@Icons.Material.Filled.Upload"
Size="Size.Small"
Color="Color.Secondary"
OnClick="@(() => LoadMeetingIntoPlanner(context))"
Variant="Variant.Text" />
</MudTooltip>
</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>
<MudTd Style="padding: 4px 8px; border-right: 2px solid var(--mud-palette-divider);"></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();
}
}
}
private async Task LoadMeetingIntoPlanner(TeamMeetingHistory history)
{
if (_isDisposed) return;
try
{
// Get all teams and students from database
var allTeams = await DataService.LoadTeamsAsync();
var allStudents = await DataService.LoadStudentsAsync();
// Match teams from history to all teams by ID for reference equality
var historyTeamIds = history.Teams.Select(t => t.Id).ToHashSet();
var scheduledTeams = allTeams.Where(t => historyTeamIds.Contains(t.Id));
// Calculate absent students (all students not in the meeting history's student list)
var presentStudentIds = history.Students.Select(s => s.Id).ToHashSet();
var absentStudents = allStudents.Where(s => !presentStudentIds.Contains(s.Id));
// Save state to localStorage
await StateService.SaveScheduledTeamsAsync(scheduledTeams);
await StateService.SaveAbsentStudentsAsync(absentStudents);
// Clear extended teams and excluded students when loading from history
await StateService.SaveExtendedTeamsAsync([]);
await StateService.SaveExcludedStudentsAsync(new Dictionary<(int teamId, int timeSlotIndex, int studentId), bool>());
// Navigate to planner
NavigationManager.NavigateTo("/meeting-schedule");
if (!_isDisposed)
{
Snackbar.Add($"Loaded meeting from {history.MeetingDate:MM/dd/yyyy} into planner", Severity.Success);
}
}
catch (TaskCanceledException)
{
// Component was disposed, ignore
}
catch (JSDisconnectedException)
{
// JS connection lost, ignore
}
catch (Exception ex)
{
if (!_isDisposed)
{
Snackbar.Add($"Error loading meeting into planner: {ex.Message}", Severity.Error);
}
}
}
public async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
_isDisposed = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
await ValueTask.CompletedTask;
}
}