SortableJS duplicated and mis-ordered events; buttons keep rank in C# and drop the unused BlazorSortableList package. Co-authored-by: Cursor <cursoragent@cursor.com>
240 lines
9.4 KiB
Plaintext
240 lines
9.4 KiB
Plaintext
@page "/students/event-ranking-edit/{StudentId:int}"
|
|
@attribute [Authorize]
|
|
@using Microsoft.EntityFrameworkCore
|
|
@using WebApp.Models
|
|
@inject AppDbContext Context
|
|
@inject NavigationManager NavigationManager
|
|
@inject ISnackbar Snackbar
|
|
@inject ILogger<EventRankingEdit> Logger
|
|
|
|
@if (_student == null)
|
|
{
|
|
<p><em>Loading...</em></p>
|
|
}
|
|
else
|
|
{
|
|
<PageHeader
|
|
Title="Student Event Ranks"
|
|
Subtitle="@_student.Name"
|
|
Icon="@AppIcons.EventRank"
|
|
ShowBackButton="true"
|
|
BackButtonUrl="/students/event-ranking">
|
|
<ActionButtons>
|
|
<MudTooltip Text="Save Rankings">
|
|
<MudButton StartIcon="@Icons.Material.Filled.Save"
|
|
OnClick="Save"
|
|
Color="Color.Primary"
|
|
Variant="Variant.Filled">
|
|
Save Rankings
|
|
</MudButton>
|
|
</MudTooltip>
|
|
</ActionButtons>
|
|
</PageHeader>
|
|
|
|
<MudGrid Class="mt-2">
|
|
<MudItem xs="12" md="6" xl="4">
|
|
<MudPaper Class="pa-3 mb-3" Elevation="2">
|
|
<div class="d-flex align-center mb-1">
|
|
<MudIcon Icon="@Icons.Material.Filled.FormatListNumbered" Class="mr-2" Color="Color.Primary" />
|
|
<MudText Typo="Typo.h6" Color="Color.Primary">Ranked Events</MudText>
|
|
</div>
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
Use the arrows to set rank. Use the X to remove an event. Up to @StudentEventRanking.MaxRank events.
|
|
</MudText>
|
|
</MudPaper>
|
|
@if (_rankedEvents.Count == 0)
|
|
{
|
|
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-2">
|
|
No ranked events yet. Add events from the list on the right.
|
|
</MudAlert>
|
|
}
|
|
@for (var i = 0; i < _rankedEvents.Count; i++)
|
|
{
|
|
var index = i;
|
|
var item = _rankedEvents[index];
|
|
var rank = index + 1;
|
|
<MudCard @key="item.Id" Outlined="true" Class="mb-2">
|
|
<MudCardContent Class="pa-2">
|
|
<div class="d-flex align-center">
|
|
<MudBadge Content="@rank" Color="Color.Primary" Overlap="true" Class="mr-3">
|
|
<MudIcon Icon="@Icons.Material.Filled.FormatListNumbered" />
|
|
</MudBadge>
|
|
<div class="flex-grow-1">
|
|
<MudText Typo="Typo.body2"><strong>@item.Name</strong></MudText>
|
|
<MudText Typo="Typo.caption">
|
|
@AppIcons.EventAttributes(item) @AppIcons.EventEffort(item)
|
|
</MudText>
|
|
</div>
|
|
<MudIconButton Icon="@Icons.Material.Filled.KeyboardArrowUp"
|
|
Size="Size.Small"
|
|
Disabled="@(index == 0)"
|
|
aria-label="Move up"
|
|
OnClick="@(() => MoveRanked(item, -1))" />
|
|
<MudIconButton Icon="@Icons.Material.Filled.KeyboardArrowDown"
|
|
Size="Size.Small"
|
|
Disabled="@(index == _rankedEvents.Count - 1)"
|
|
aria-label="Move down"
|
|
OnClick="@(() => MoveRanked(item, 1))" />
|
|
<MudIconButton Icon="@Icons.Material.Filled.Close"
|
|
Size="Size.Small"
|
|
Color="Color.Default"
|
|
aria-label="Remove from ranked events"
|
|
OnClick="@(() => RemoveRanked(item))" />
|
|
</div>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
</MudItem>
|
|
<MudItem xs="12" md="6" xl="4">
|
|
<MudPaper Class="pa-3 mb-3" Elevation="2">
|
|
<div class="d-flex align-center mb-1">
|
|
<MudIcon Icon="@AppIcons.Events" Class="mr-2" />
|
|
<MudText Typo="Typo.h6">Available Events</MudText>
|
|
</div>
|
|
<MudText Typo="Typo.caption" Color="Color.Secondary">
|
|
@if (AtMaxRank)
|
|
{
|
|
<text>Maximum of @StudentEventRanking.MaxRank ranked events reached. Remove one to add another.</text>
|
|
}
|
|
else
|
|
{
|
|
<text>Use Add to place an event at the end of the ranked list.</text>
|
|
}
|
|
</MudText>
|
|
</MudPaper>
|
|
@if (_availableEvents.Count == 0)
|
|
{
|
|
<MudAlert Severity="Severity.Info" Dense="true">
|
|
@(_rankedEvents.Count == 0 ? "No events are available to rank." : "All events are already ranked.")
|
|
</MudAlert>
|
|
}
|
|
@foreach (var item in _availableEvents)
|
|
{
|
|
<MudCard @key="item.Id" Outlined="true" Class="mb-2">
|
|
<MudCardContent Class="pa-2">
|
|
<div class="d-flex align-center">
|
|
<div class="flex-grow-1">
|
|
<MudText Typo="Typo.body2"><strong>@item.Name</strong></MudText>
|
|
<MudText Typo="Typo.caption">
|
|
@AppIcons.EventAttributes(item) @AppIcons.EventEffort(item)
|
|
</MudText>
|
|
</div>
|
|
<MudIconButton Icon="@Icons.Material.Filled.Add"
|
|
Color="Color.Primary"
|
|
Disabled="@AtMaxRank"
|
|
aria-label="@($"Add {item.Name} to ranked events")"
|
|
OnClick="@(() => AddRanked(item))" />
|
|
</div>
|
|
</MudCardContent>
|
|
</MudCard>
|
|
}
|
|
</MudItem>
|
|
</MudGrid>
|
|
}
|
|
|
|
@code {
|
|
|
|
[Parameter] public int? StudentId { get; set; }
|
|
|
|
private Student? _student;
|
|
|
|
private List<EventDefinition> _rankedEvents = [];
|
|
private List<EventDefinition> _availableEvents = [];
|
|
|
|
private bool AtMaxRank => _rankedEvents.Count >= StudentEventRanking.MaxRank;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
_student =
|
|
await Context.Students
|
|
.Include(e => e.EventRankings)
|
|
.ThenInclude(r => r.EventDefinition)
|
|
.Where(e => e.Id == StudentId)
|
|
.FirstAsync();
|
|
|
|
var events =
|
|
await Context.Events
|
|
.OrderBy(e => e.Name)
|
|
.ToListAsync();
|
|
|
|
_rankedEvents = _student.EventRankings
|
|
.Where(e => e.EventDefinition != null)
|
|
.OrderBy(e => e.Rank)
|
|
.Select(e => e.EventDefinition)
|
|
.DistinctBy(e => e.Id)
|
|
.Take(StudentEventRanking.MaxRank)
|
|
.ToList();
|
|
|
|
var rankedIds = _rankedEvents.Select(e => e.Id).ToHashSet();
|
|
_availableEvents = events.Where(e => !rankedIds.Contains(e.Id)).ToList();
|
|
}
|
|
|
|
private void AddRanked(EventDefinition item)
|
|
{
|
|
if (AtMaxRank || _rankedEvents.Any(e => e.Id == item.Id))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_availableEvents.RemoveAll(e => e.Id == item.Id);
|
|
_rankedEvents.Add(item);
|
|
}
|
|
|
|
private void RemoveRanked(EventDefinition item)
|
|
{
|
|
_rankedEvents.RemoveAll(e => e.Id == item.Id);
|
|
if (_availableEvents.All(e => e.Id != item.Id))
|
|
{
|
|
_availableEvents.Add(item);
|
|
_availableEvents.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
}
|
|
|
|
private void MoveRanked(EventDefinition item, int offset)
|
|
{
|
|
var index = _rankedEvents.FindIndex(e => e.Id == item.Id);
|
|
var newIndex = index + offset;
|
|
if (index < 0 || newIndex < 0 || newIndex >= _rankedEvents.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_rankedEvents.RemoveAt(index);
|
|
_rankedEvents.Insert(newIndex, item);
|
|
}
|
|
|
|
async Task Save()
|
|
{
|
|
if (_student == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
var uniqueRanked = _rankedEvents.DistinctBy(e => e.Id).Take(StudentEventRanking.MaxRank).ToList();
|
|
|
|
_student.EventRankings.Clear();
|
|
for (var index = 0; index < uniqueRanked.Count; index++)
|
|
{
|
|
var evt = uniqueRanked[index];
|
|
_student.EventRankings.Add(new StudentEventRanking
|
|
{
|
|
EventDefinition = evt,
|
|
Student = _student,
|
|
Rank = index + 1
|
|
});
|
|
}
|
|
|
|
Context.Students.Update(_student);
|
|
await Context.SaveChangesAsync();
|
|
|
|
Snackbar.Add($"Event rankings saved for {_student.Name}", Severity.Success);
|
|
NavigationManager.NavigateTo("/students/event-ranking");
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Snackbar.Add("Error saving rankings. Please try again.", Severity.Error);
|
|
Logger.LogError(e, "Error saving event rankings for student {StudentId}", StudentId);
|
|
}
|
|
}
|
|
}
|