Files
chapter-organizer/WebApp/Components/Features/Teams/Index.razor
T
poprhythm aba8ea3ae3 Refactor event, student, and team deletion logic to improve tracking and error handling
This commit updates the deletion logic in the Index.razor components for Events, Students, and Teams to load the entities fresh from the database with tracking. This change prevents tracking conflicts and ensures that the correct entity is deleted. Additionally, it adds error handling to notify users if the entity is not found or has already been deleted, enhancing the user experience and reliability of the deletion process.
2026-01-13 10:01:37 -05:00

320 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>
<MudButton StartIcon="@Icons.Material.Filled.Create" Href="teams/create" Variant="Variant.Filled" Color="Color.Primary">Create New</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Print" Href="teams/printout" Variant="Variant.Outlined">Printout</MudButton>
<MudButton StartIcon="@Icons.Material.Filled.Print" Href="teams/handout" Variant="Variant.Outlined">Handout</MudButton>
<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>
</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">
<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;
}
}