Files
chapter-organizer/WebApp/Components/Features/MeetingSchedule/History.razor
T
poprhythm 804e12ca22 Refactor History component in MeetingSchedule to enhance table structure and styling
This commit updates the History component by replacing the MudTable with a custom HTML table structure for improved styling and responsiveness. The table now features enhanced column definitions, including vertical text orientation for headers and improved row styling for better readability. Additionally, CSS styles are introduced to manage table appearance, ensuring a more user-friendly interface for displaying meeting histories. This enhancement contributes to a better overall user experience in the meeting schedule feature.
2026-01-25 21:16:26 -05:00

398 lines
15 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; -webkit-overflow-scrolling: touch;">
<table class="history-grid-table" style="min-width: max-content; width: 100%; border-collapse: collapse;">
<colgroup>
<col style="width: 72px;" />
<col style="width: 40px;" />
@for (var c = 0; c < _allTeams.Count; c++)
{
<col style="width: 28px;" />
}
</colgroup>
<thead>
<tr>
<th class="history-cell history-cell-date">
<span class="mud-typography mud-typography-caption">Date</span>
</th>
<th class="history-cell history-cell-actions">
<span class="mud-typography mud-typography-caption" style="writing-mode: vertical-rl; text-orientation: mixed; transform: rotate(180deg);">Actions</span>
</th>
@{
var teamIndex = 0;
}
@foreach (var team in _allTeams)
{
var isEven = teamIndex % 2 == 0;
var colClass = isEven ? "history-cell history-cell-col-even" : "history-cell history-cell-col-odd";
<th class="@colClass">
<span class="mud-typography mud-typography-caption" style="writing-mode: vertical-rl; text-orientation: mixed; transform: rotate(180deg);">@team.ToString()</span>
</th>
teamIndex++;
}
</tr>
<tr>
<th class="history-cell history-cell-date history-cell-times-met">Times Met</th>
<th class="history-cell history-cell-actions "></th>
@{
var summaryTeamIndex = 0;
}
@foreach (var team in _allTeams)
{
var timesMet = GetTimesMetForTeam(team);
var isEven = summaryTeamIndex % 2 == 0;
var colClass = isEven ? "history-cell history-cell-col-even history-cell-times-met" : "history-cell history-cell-col-odd history-cell-times-met";
<th class="@colClass">@timesMet</th>
summaryTeamIndex++;
}
</tr>
</thead>
<tbody>
@{
var rowIndex = 0;
}
@foreach (var history in _meetingHistories)
{
var rowTeamIndex = 0;
var rowClass = rowIndex % 2 == 0 ? "history-row-even" : "history-row-odd";
<tr class="@rowClass">
<td class="history-cell history-cell-date ">
<MudButton Variant="Variant.Text"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => ViewMeetingDetails(history))"
Style="text-transform: none; padding: 0 2px; min-width: unset;">
@history.MeetingDate.ToString("MM/dd/yy")
</MudButton>
</td>
<td class="history-cell history-cell-actions">
<MudTooltip Text="Load into Planner">
<MudIconButton Icon="@Icons.Material.Filled.Upload"
Size="Size.Small"
Color="Color.Secondary"
OnClick="@(() => LoadMeetingIntoPlanner(history))"
Variant="Variant.Text"
Style="padding: 2px;" />
</MudTooltip>
</td>
@foreach (var team in _allTeams)
{
var met = TeamMetOnDate(history, team);
var isEven = rowTeamIndex % 2 == 0;
var colClass = isEven ? "history-cell history-cell-col-even" : "history-cell history-cell-col-odd";
<td class="@colClass">
@if (met)
{
<span class="mud-typography mud-typography-body1" style="font-weight: bold;">×</span>
}
</td>
rowTeamIndex++;
}
</tr>
rowIndex++;
}
</tbody>
</table>
</MudPaper>
}
else
{
<MudAlert Severity="Severity.Info">No meeting history found. Save a meeting schedule to get started.</MudAlert>
}
</MudStack>
}
</MudPaper>
<style>
.history-grid-table {
--history-border: 1px solid var(--mud-palette-divider);
--history-shade: var(--mud-palette-background-grey);
}
.history-grid-table th,
.history-grid-table td {
padding: 2px 4px;
border-right: var(--history-border);
border-bottom: var(--history-border);
vertical-align: middle;
}
.history-grid-table thead th {
border-top: var(--history-border);
background-color: var(--mud-palette-surface);
}
.history-grid-table thead .history-cell-col-odd {
background-color: var(--mud-palette-surface);
}
.history-grid-table thead th:first-child,
.history-grid-table tbody td:first-child {
border-left: var(--history-border);
}
.history-grid-table .history-cell-date {
padding: 2px 4px;
text-align: left;
min-width: 72px;
}
.history-grid-table .history-cell-actions {
padding: 2px;
text-align: center;
min-width: 40px;
}
.history-grid-table thead .history-cell-col-even {
background-color: var(--history-shade);
}
.history-grid-table .history-cell-times-met {
font-weight: bold;
background-color: var(--history-shade);
}
.history-grid-table tbody .history-row-odd td {
background-color: var(--history-shade);
}
.history-grid-table tbody .history-row-even td.history-cell-col-even,
.history-grid-table tbody .history-row-odd td.history-cell-col-even {
background-color: var(--history-shade);
}
.history-grid-table tbody .history-row-odd td.history-cell-col-odd {
background-color: var(--history-shade);
}
</style>
@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;
}
}