Files
chapter-organizer/WebApp/Components/Features/Events/Edit.razor
T
poprhythm 06b2db0b4c Add Related Careers functionality to EventDefinition entity and update related components
Introduced a many-to-many relationship between EventDefinition and Career entities, allowing for the association of multiple careers with an event. Updated the AppDbContext to include a DbSet for Careers and modified the EventDefinitionConfiguration to handle the new relationship. Enhanced the Create, Edit, and Details components to support input and display of related careers, including normalization and processing logic for career names. Updated the database schema to reflect these changes.
2025-12-28 15:22:03 -05:00

246 lines
11 KiB
Plaintext

@page "/events/edit"
@attribute [Authorize]
@using Microsoft.EntityFrameworkCore
@using WebApp.Components.Shared.Components
@using Core.Utility
@inject AppDbContext context
@inject NavigationManager NavigationManager
@if (EventDefinition is null)
{
<p><em>Loading...</em></p>
return;
}
<PageHeader
Title="Edit"
Subtitle="Event"
ShowBackButton="true"
BackButtonUrl="@(ReturnUrl ?? "/events")" />
<EditForm Model="EventDefinition" OnValidSubmit="OnValidSubmit" Enhance>
<FormChangeTracker @ref="_formChangeTracker" />
<AntiforgeryToken />
<DataAnnotationsValidator />
<MudStack Spacing="4">
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">Basic Information</MudText>
<MudGrid Spacing="3">
<MudItem xs="12" sm="6">
<MudTextField T="string" Label="Event Name" @bind-Value="EventDefinition.Name" For="@(() => EventDefinition.Name)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12" sm="6">
<MudTextField T="string" Label="Short Name" @bind-Value="EventDefinition.ShortName" For="@(() => EventDefinition.ShortName)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.subtitle2" Class="mb-2 mud-text-secondary">Event Format</MudText>
<MudRadioGroup T="EventFormat" @bind-Value="@EventDefinition.EventFormat" For="@(() => EventDefinition.EventFormat)">
@foreach (EventFormat format in Enum.GetValues(typeof(EventFormat)))
{
<MudRadio T="EventFormat" Value="@format">@format.ToString()</MudRadio>
}
</MudRadioGroup>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField T="int?" Label="Level of Effort" @bind-Value="EventDefinition.LevelOfEffort" For="@(() => EventDefinition.LevelOfEffort)" Variant="Variant.Outlined"></MudNumericField>
</MudItem>
</MudGrid>
</MudPaper>
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">Event Details</MudText>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudTextField T="string" Label="Description" AutoGrow="true" Lines="3" @bind-Value="EventDefinition.Description" For="@(() => EventDefinition.Description)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Theme" AutoGrow="true" Lines="2" @bind-Value="EventDefinition.Theme" For="@(() => EventDefinition.Theme)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Documentation" @bind-Value="EventDefinition.Documentation" For="@(() => EventDefinition.Documentation)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Related Careers" AutoGrow="true" Lines="5" @bind-Value="EventDefinition.RelatedCareersText" HelperText="Enter one career per line (bullet points will be removed automatically)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
</MudGrid>
</MudPaper>
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">Team Configuration</MudText>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudTextField T="string" Label="Nationals Eligibility" @bind-Value="EventDefinition.Eligibility" For="@(() => EventDefinition.Eligibility)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField T="int" Label="Minimum Team Size" @bind-Value="EventDefinition.MinTeamSize" For="@(() => EventDefinition.MinTeamSize)" Variant="Variant.Outlined"></MudNumericField>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField T="int" Label="Maximum Team Size" @bind-Value="EventDefinition.MaxTeamSize" For="@(() => EventDefinition.MaxTeamSize)" Variant="Variant.Outlined"></MudNumericField>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField T="int" Label="Team Count at Regionals" @bind-Value="EventDefinition.ChapterEligibilityCountRegionals" For="@(() => EventDefinition.ChapterEligibilityCountRegionals)" Variant="Variant.Outlined"></MudNumericField>
</MudItem>
<MudItem xs="12" sm="6">
<MudNumericField T="int" Label="Team Count at State" @bind-Value="EventDefinition.ChapterEligibilityCountState" For="@(() => EventDefinition.ChapterEligibilityCountState)" Variant="Variant.Outlined"></MudNumericField>
</MudItem>
</MudGrid>
</MudPaper>
<MudPaper Elevation="2" Class="pa-6">
<MudText Typo="Typo.h5" Class="mb-4">Competition Details</MudText>
<MudGrid Spacing="3">
<MudItem xs="12">
<MudTextField T="string" Label="Semifinalist Activity" @bind-Value="EventDefinition.SemifinalistActivity" For="@(() => EventDefinition.SemifinalistActivity)" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="On Site Activity" @bind-Value="EventDefinition.OnSiteActivity" For="@(() => EventDefinition.OnSiteActivity)"></MudCheckBox>
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Requires Presubmission" @bind-Value="EventDefinition.Presubmission" For="@(() => EventDefinition.Presubmission)"></MudCheckBox>
</MudItem>
</MudGrid>
<FormActions>
<MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary">Save</MudButton>
<MudButton OnClick="HandleCancel" Variant="Variant.Text">Cancel</MudButton>
</FormActions>
</MudPaper>
</MudStack>
</EditForm>
@code {
[SupplyParameterFromQuery]
private int Id { get; set; }
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
[SupplyParameterFromForm]
private EventDefinition? EventDefinition { get; set; }
private FormChangeTracker? _formChangeTracker;
protected override async Task OnInitializedAsync()
{
EventDefinition ??= await context.Events
.Include(e => e.RelatedCareers)
.FirstOrDefaultAsync(m => m.Id == Id);
if (EventDefinition is null)
{
NavigationManager.NavigateTo("notfound");
}
else
{
// Populate RelatedCareersText from RelatedCareers collection
EventDefinition.RelatedCareersText = string.Join("\n", EventDefinition.RelatedCareers.Select(c => c.Name));
}
}
// To protect from overposting attacks, enable the specific properties you want to bind to.
// For more information, see https://learn.microsoft.com/aspnet/core/blazor/forms/#mitigate-overposting-attacks.
private async Task OnValidSubmit()
{
// Get the tracked entity from the database
var trackedEntity = await context.Events
.Include(e => e.RelatedCareers)
.FirstOrDefaultAsync(e => e.Id == EventDefinition!.Id);
if (trackedEntity == null)
{
NavigationManager.NavigateTo("notfound");
return;
}
// Update scalar properties from the form-bound entity
context.Entry(trackedEntity).CurrentValues.SetValues(EventDefinition!);
// Normalize and process related careers
await ProcessRelatedCareersAsync(trackedEntity);
try
{
await context.SaveChangesAsync();
_formChangeTracker?.AllowNavigation();
NavigationManager.NavigateTo(ReturnUrl ?? "/events");
}
catch (DbUpdateConcurrencyException)
{
if (!EventDefinitionExists(EventDefinition!.Id))
{
NavigationManager.NavigateTo("notfound");
}
else
{
throw;
}
}
}
private async Task ProcessRelatedCareersAsync(EventDefinition eventDefinition)
{
if (string.IsNullOrWhiteSpace(eventDefinition.RelatedCareersText))
{
eventDefinition.RelatedCareers.Clear();
return;
}
var normalizedNames = CareerNormalizer.NormalizeCareerNames(eventDefinition.RelatedCareersText).ToList();
if (!normalizedNames.Any())
{
eventDefinition.RelatedCareers.Clear();
return;
}
// Get all existing careers from database (case-insensitive lookup)
var existingCareers = await context.Careers.ToListAsync();
var careerLookup = existingCareers.ToDictionary(
c => CareerNormalizer.GetNormalizedKey(c.Name),
c => c,
StringComparer.OrdinalIgnoreCase);
var careersToAdd = new List<Career>();
foreach (var normalizedName in normalizedNames)
{
var normalizedKey = CareerNormalizer.GetNormalizedKey(normalizedName);
if (careerLookup.TryGetValue(normalizedKey, out var existingCareer))
{
// Use existing career (preserve original capitalization)
careersToAdd.Add(existingCareer);
}
else
{
// Create new career with the normalized name (preserving capitalization from input)
var newCareer = new Career { Name = normalizedName };
context.Careers.Add(newCareer);
careersToAdd.Add(newCareer);
careerLookup[normalizedKey] = newCareer;
}
}
// Replace the collection
eventDefinition.RelatedCareers = careersToAdd;
}
private async Task HandleCancel()
{
// Discard all in-memory changes by reloading from database
if (EventDefinition != null)
{
await context.Entry(EventDefinition).ReloadAsync();
}
_formChangeTracker?.AllowNavigation();
NavigationManager.NavigateTo(ReturnUrl ?? "/events");
}
private bool EventDefinitionExists(int id)
{
return context.Events.Any(e => e.Id == id);
}
}