@page "/students"
@attribute [Authorize]
@implements IAsyncDisposable
@using Microsoft.EntityFrameworkCore
@using WebApp.Authentication
@using WebApp.Models
@using WebApp.Components.Shared.Components
@inject AppDbContext Context
@inject IDialogService DialogService
@inject ISnackbar Snackbar
@inject INotesService NotesService
@inject IConfiguration Configuration
@inject IJSRuntime JSRuntime
@using Core.Notes
@using Core.Parsers
@using WebApp.Services
Create New
Import
Download CSV template
Event Rankings
Registration
@context.Item.LastNameFirstName
@if (context.Item.OfficerRole != null)
{
@context.Item.OfficerRole
}
@((MarkupString)AppIcons.GetOrdinalSuperscript(context.Item.Grade)) (@context.Item.TsaYear)
@foreach (var field in _noteFieldColumns)
{
var fieldName = field;
@GetNoteField(context.Item.Id, fieldName)
}
@code {
MudDataGrid _dataGrid = null!;
private bool _isLoading = true;
private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed = false;
private List _noteFieldColumns = [];
private Dictionary _noteContentByStudentId = [];
private string NoteFieldColumnsKey => string.Join('\u001f', _noteFieldColumns);
protected override void OnInitialized()
{
_cancellationTokenSource = new CancellationTokenSource();
}
protected override void OnParametersSet()
{
_noteFieldColumns = WebApp.Models.ChapterSettings.ReadIndexNoteFields(Configuration);
}
private async Task> ServerReload(GridState state)
{
if (_isDisposed)
{
return new GridData { TotalItems = 0, Items = [] };
}
_isLoading = true;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
var query =
Context.Students
.AsNoTracking()
.OrderBy(e => e.LastName)
.Where(state.FilterDefinitions).OrderBy(state.SortDefinitions);
var totalItems = await query.CountAsync(cancellationToken);
var pagedData = await query.Skip(state.Page * state.PageSize).Take(state.PageSize).ToArrayAsync(cancellationToken);
var notes = await NotesService.GetStudentNotesAsync(pagedData.Select(s => s.Id));
_noteContentByStudentId = notes.ToDictionary(k => k.Key, v => v.Value.Content);
return new GridData
{
TotalItems = totalItems,
Items = pagedData
};
}
catch (TaskCanceledException)
{
return new GridData { TotalItems = 0, Items = [] };
}
catch (JSDisconnectedException)
{
return new GridData { TotalItems = 0, Items = [] };
}
finally
{
if (!_isDisposed)
{
_isLoading = false;
}
}
}
private async Task DeleteStudent(Student student)
{
if (_isDisposed) return;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
var result = await DialogService
.ShowMessageBox("Delete student",
(MarkupString)$"Are you sure want to delete {student.Name}? This cannot be undone.",
yesText:"Yes",
noText:"Cancel");
if (_isDisposed) return;
if (result == true)
{
// Load the student fresh from database with tracking to avoid tracking conflicts
var studentToDelete = await Context.Students
.FirstOrDefaultAsync(s => s.Id == student.Id, cancellationToken);
if (_isDisposed) return;
if (studentToDelete == null)
{
if (!_isDisposed)
{
Snackbar.Add("Student not found or already deleted", Severity.Warning);
}
return;
}
await NotesService.SoftDeleteStudentNotesAsync([studentToDelete.Id], cancellationToken);
Context.Students.Remove(studentToDelete);
await Context.SaveChangesAsync(cancellationToken);
if (!_isDisposed)
{
Snackbar.Add($"Student {studentToDelete.Name} 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 student: {ex.Message}", Severity.Error);
}
}
}
public async ValueTask DisposeAsync()
{
if (!_isDisposed)
{
_isDisposed = true;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
await ValueTask.CompletedTask;
}
private async Task DownloadStudentImportTemplate()
{
if (_isDisposed)
return;
try
{
var cancellationToken = _cancellationTokenSource?.Token ?? CancellationToken.None;
var fromNotes = await NotesService.GetImportedFieldNamesAsync(cancellationToken);
var leftover = _noteFieldColumns
.Concat(fromNotes)
.Distinct(StringComparer.OrdinalIgnoreCase);
var csv = StudentImportCsvTemplate.Build(leftover);
byte[] bytes = [..System.Text.Encoding.UTF8.GetPreamble(), ..System.Text.Encoding.UTF8.GetBytes(csv)];
var base64 = Convert.ToBase64String(bytes);
await JSRuntime.InvokeVoidAsync("tsaDownload.fromBase64", "student-import-template.csv", "text/csv;charset=utf-8", base64);
}
catch (JSDisconnectedException)
{
}
catch (TaskCanceledException)
{
}
catch (Exception ex)
{
if (!_isDisposed)
Snackbar.Add($"Could not download template: {ex.Message}", Severity.Error);
}
}
private string GetNoteField(int studentId, string fieldName)
{
if (!_noteContentByStudentId.TryGetValue(studentId, out var content))
return string.Empty;
return ImportedFieldsTable.GetFieldValue(content, fieldName) ?? string.Empty;
}
}