a503655f97
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.
350 lines
14 KiB
Plaintext
350 lines
14 KiB
Plaintext
@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;
|
||
}
|
||
}
|