@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; } }