15d7edec8f
This commit introduces a new feature to display meeting history for teams. It adds a `TeamMeetingHistoryDialog` component that shows the meeting history in a dialog format, including a loading state and error handling. Additionally, a `TeamMeetingHistoryBadge` component is created to display the count of meetings for each team, enhancing the user interface with tooltips for better interaction. The `Details` and `Index` components are updated to integrate these new features, allowing users to view meeting history directly from the team details and index pages. These changes improve the overall functionality and user experience in managing team meetings.
330 lines
12 KiB
Plaintext
330 lines
12 KiB
Plaintext
@using Microsoft.EntityFrameworkCore
|
|
@using WebApp.Components.Shared.Components
|
|
@using WebApp.Models
|
|
@page "/teams"
|
|
@attribute [Authorize]
|
|
@implements IAsyncDisposable
|
|
@inject AppDbContext Context
|
|
@inject IDialogService DialogService
|
|
@inject ISnackbar Snackbar
|
|
|
|
<PageHeader Title="Teams">
|
|
<ActionButtons>
|
|
<MudTooltip Text="Create New">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Create" Href="teams/create" Variant="Variant.Filled" Color="Color.Primary">Create New</MudButton>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Printout">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Print" Href="teams/printout" Variant="Variant.Outlined">Printout</MudButton>
|
|
</MudTooltip>
|
|
<MudTooltip Text="Handout">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Print" Href="teams/handout" Variant="Variant.Outlined">Handout</MudButton>
|
|
</MudTooltip>
|
|
<MudTooltip Text="@(_showRegionalOnly ? "Showing Regional Only" : "Show Regional Only")">
|
|
<MudButton StartIcon="@Icons.Material.Filled.FilterAlt"
|
|
Variant="@(_showRegionalOnly ? Variant.Filled : Variant.Outlined)"
|
|
Color="Color.Primary"
|
|
OnClick="ToggleRegionalFilter">
|
|
@(_showRegionalOnly ? "Showing Regional Only" : "Show Regional Only")
|
|
</MudButton>
|
|
</MudTooltip>
|
|
<PageNoteButton PageIdentifier="Teams" />
|
|
</ActionButtons>
|
|
</PageHeader>
|
|
|
|
<MudPaper Elevation="2" Class="pa-3 pa-md-6">
|
|
<MudDataGrid T="Team"
|
|
ServerData="ServerReload"
|
|
@ref="_dataGrid"
|
|
Filterable="true"
|
|
RowsPerPage="50"
|
|
Dense="true"
|
|
Striped="true"
|
|
Hover="true"
|
|
Loading="@_isLoading"
|
|
LoadingProgressColor="Color.Primary">
|
|
<Columns>
|
|
<TemplateColumn Title="Event" Sortable="true" SortBy="@(t => t.Event.Name)">
|
|
<CellTemplate>
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Spacing="1">
|
|
<MudLink Href="@($"/teams/details?id={context.Item.Id}&returnUrl=/teams")"
|
|
Underline="Underline.Hover"
|
|
Color="Color.Primary">
|
|
@context.Item.ToString()
|
|
</MudLink>
|
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
|
|
<TeamMeetingHistoryBadge TeamId="@context.Item.Id" TeamName="@context.Item.ToString()" />
|
|
<IconButtonWithTooltip Icon="@Icons.Material.Filled.Edit"
|
|
TooltipText="Edit"
|
|
Href="@($"/teams/edit?id={context.Item.Id}&returnUrl=/teams")" />
|
|
<IconButtonWithTooltip Icon="@Icons.Material.Outlined.Delete"
|
|
TooltipText="Delete"
|
|
HoverColor="Color.Error"
|
|
OnClick="() => DeleteTeam(context.Item!)" />
|
|
</MudStack>
|
|
</MudStack>
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
<TemplateColumn Title="Attributes">
|
|
<CellTemplate>
|
|
<EventAttributes EventDefinition="@context.Item.Event"></EventAttributes>
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
<TemplateColumn Title="Students">
|
|
<CellTemplate>
|
|
<TeamStudents Team="@context.Item"></TeamStudents>
|
|
</CellTemplate>
|
|
</TemplateColumn>
|
|
</Columns>
|
|
<PagerContent>
|
|
<MudDataGridPager T="Team"></MudDataGridPager>
|
|
</PagerContent>
|
|
</MudDataGrid>
|
|
</MudPaper>
|
|
|
|
@code {
|
|
MudDataGrid<Team> _dataGrid = null!;
|
|
private bool _isLoading = true;
|
|
private bool _showRegionalOnly = false;
|
|
private CancellationTokenSource? _cancellationTokenSource;
|
|
private bool _isDisposed = false;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
_cancellationTokenSource = new CancellationTokenSource();
|
|
}
|
|
|
|
private async Task ToggleRegionalFilter()
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
try
|
|
{
|
|
_showRegionalOnly = !_showRegionalOnly;
|
|
if (_dataGrid != null && !_isDisposed)
|
|
{
|
|
await _dataGrid.ReloadServerData();
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
// Component was disposed, ignore
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// JS connection lost, ignore
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Error applying filter: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<GridData<Team>> ServerReload(GridState<Team> state)
|
|
{
|
|
if (_isDisposed)
|
|
{
|
|
return new GridData<Team> { TotalItems = 0, Items = [] };
|
|
}
|
|
|
|
_isLoading = true;
|
|
try
|
|
{
|
|
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
|
|
IQueryable<Team> query
|
|
= Context.Teams
|
|
.AsNoTracking()
|
|
.Include(e => e.Event)
|
|
.Include(e => e.Captain)
|
|
.Include(e => e.Students)
|
|
.ThenInclude(e => e.EventRankings);
|
|
|
|
// Apply regional filter if enabled
|
|
// Note: RegionalEvent is a computed property, so we must use the underlying property
|
|
if (_showRegionalOnly)
|
|
{
|
|
query = query.Where(t => t.Event.ChapterEligibilityCountRegionals > 0);
|
|
}
|
|
|
|
// Apply grid filter definitions
|
|
query = query.Where(state.FilterDefinitions);
|
|
|
|
// Load all data first
|
|
var allTeams = await query.ToArrayAsync(cancellationToken);
|
|
|
|
// Always sort by EventFormat FIRST to separate group/individual teams
|
|
// Sort in memory to ensure this ordering is maintained regardless of user sorts
|
|
var sortedTeams = allTeams.OrderByEventFormatFirst();
|
|
|
|
// Apply user sort definitions as secondary sorts (within each group)
|
|
bool appliedUserSort = false;
|
|
if (state.SortDefinitions != null && state.SortDefinitions.Any())
|
|
{
|
|
var sortDef = state.SortDefinitions.First();
|
|
var sortBy = sortDef.SortBy ?? "";
|
|
|
|
// Handle Event.Name sorting
|
|
if (sortBy == "Event.Name" || (sortBy.Contains("Event") && sortBy.Contains("Name")))
|
|
{
|
|
sortedTeams = sortDef.Descending
|
|
? sortedTeams.ThenByDescending(t => t.Event.Name)
|
|
: sortedTeams.ThenBy(t => t.Event.Name);
|
|
appliedUserSort = true;
|
|
}
|
|
// Handle Identifier sorting
|
|
else if (sortBy == "Identifier")
|
|
{
|
|
sortedTeams = sortDef.Descending
|
|
? sortedTeams.ThenByDescending(t => t.Identifier ?? "")
|
|
: sortedTeams.ThenBy(t => t.Identifier ?? "");
|
|
appliedUserSort = true;
|
|
}
|
|
}
|
|
|
|
// Apply default secondary sorting
|
|
if (!appliedUserSort)
|
|
{
|
|
sortedTeams = sortedTeams
|
|
.ThenBy(t => t.Event.Name)
|
|
.ThenBy(t => t.Identifier ?? "");
|
|
}
|
|
else
|
|
{
|
|
// Add default sorting as tie-breakers
|
|
if (state.SortDefinitions?.First().SortBy != "Event.Name")
|
|
{
|
|
sortedTeams = sortedTeams.ThenBy(t => t.Event.Name);
|
|
}
|
|
if (state.SortDefinitions?.First().SortBy != "Identifier")
|
|
{
|
|
sortedTeams = sortedTeams.ThenBy(t => t.Identifier ?? "");
|
|
}
|
|
}
|
|
|
|
var totalItems = sortedTeams.Count();
|
|
var pagedData = sortedTeams.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArray();
|
|
|
|
return new GridData<Team>
|
|
{
|
|
TotalItems = totalItems,
|
|
Items = pagedData
|
|
};
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
// Component was disposed, return empty result
|
|
return new GridData<Team> { TotalItems = 0, Items = [] };
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// JS connection lost, return empty result
|
|
return new GridData<Team> { TotalItems = 0, Items = [] };
|
|
}
|
|
finally
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
_isLoading = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task DeleteTeam(Team team)
|
|
{
|
|
if (_isDisposed) return;
|
|
|
|
try
|
|
{
|
|
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
|
|
|
|
var result = await DialogService
|
|
.ShowMessageBox("Delete team",
|
|
(MarkupString)$"Are you sure want to delete <b>{team}</b>? This cannot be undone.",
|
|
yesText: "Yes",
|
|
noText: "Cancel");
|
|
|
|
if (_isDisposed) return;
|
|
|
|
if (result == true)
|
|
{
|
|
// Load the team fresh from database with tracking to avoid tracking conflicts
|
|
var teamToDelete = await Context.Teams
|
|
.Include(t => t.Event)
|
|
.Include(t => t.Students)
|
|
.FirstOrDefaultAsync(t => t.Id == team.Id, cancellationToken);
|
|
|
|
if (_isDisposed) return;
|
|
|
|
if (teamToDelete == null)
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add("Team not found or already deleted", Severity.Warning);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If deleting a numbered team (1 or 2), clear the identifier of the remaining team
|
|
if (teamToDelete.Identifier == "1" || teamToDelete.Identifier == "2")
|
|
{
|
|
var remainingTeam = await Context.Teams
|
|
.Include(t => t.Event)
|
|
.FirstOrDefaultAsync(t => t.Event.Id == teamToDelete.Event.Id && t.Id != teamToDelete.Id, cancellationToken);
|
|
|
|
if (_isDisposed) return;
|
|
|
|
if (remainingTeam != null)
|
|
{
|
|
remainingTeam.Identifier = null;
|
|
Context.Teams.Update(remainingTeam);
|
|
}
|
|
}
|
|
|
|
Context.Teams.Remove(teamToDelete);
|
|
await Context.SaveChangesAsync(cancellationToken);
|
|
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Team {teamToDelete} deleted", Severity.Info);
|
|
}
|
|
}
|
|
|
|
if (!_isDisposed)
|
|
{
|
|
StateHasChanged();
|
|
await _dataGrid.ReloadServerData();
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
// Component was disposed, ignore
|
|
}
|
|
catch (JSDisconnectedException)
|
|
{
|
|
// JS connection lost, ignore
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
Snackbar.Add($"Error deleting team: {ex.Message}", Severity.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (!_isDisposed)
|
|
{
|
|
_isDisposed = true;
|
|
_cancellationTokenSource?.Cancel();
|
|
_cancellationTokenSource?.Dispose();
|
|
_cancellationTokenSource = null;
|
|
}
|
|
await ValueTask.CompletedTask;
|
|
}
|
|
}
|