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.
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Core.Entities;
|
||||
|
||||
public class Career
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(200, MinimumLength = 1)]
|
||||
[Display(Name = "Career Name")]
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +79,11 @@ public class EventDefinition
|
||||
public string? Description { get; set; }
|
||||
public int? LevelOfEffort { get; set; }
|
||||
|
||||
public ICollection<Career> RelatedCareers { get; set; } = new List<Career>();
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Schema.NotMapped]
|
||||
public string? RelatedCareersText { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Core.Utility;
|
||||
|
||||
public static class CareerNormalizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes career names from multiline text input.
|
||||
/// Strips bullet points, trims whitespace, and returns distinct normalized names.
|
||||
/// </summary>
|
||||
/// <param name="input">Multiline text input containing career names</param>
|
||||
/// <returns>Collection of normalized career names (trimmed, with bullets removed)</returns>
|
||||
public static IEnumerable<string> NormalizeCareerNames(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
return input
|
||||
.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None)
|
||||
.Select(line => NormalizeSingleCareerName(line))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a single career name by stripping bullet points and trimming whitespace.
|
||||
/// </summary>
|
||||
/// <param name="careerName">The career name to normalize</param>
|
||||
/// <returns>Normalized career name</returns>
|
||||
private static string NormalizeSingleCareerName(string careerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(careerName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Remove common bullet point characters (•, -, *, etc.) and trim
|
||||
var normalized = Regex.Replace(careerName.Trim(), @"^[\u2022\u2023\u25E6\u2043\u2219\-\*\•]\s*", string.Empty, RegexOptions.Compiled);
|
||||
|
||||
return normalized.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds or creates a career name for case-insensitive duplicate detection.
|
||||
/// Returns the normalized (lowercase) version for comparison.
|
||||
/// </summary>
|
||||
/// <param name="careerName">The career name to normalize for comparison</param>
|
||||
/// <returns>Lowercase normalized name for duplicate detection</returns>
|
||||
public static string GetNormalizedKey(string careerName)
|
||||
{
|
||||
return NormalizeSingleCareerName(careerName).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ namespace Data
|
||||
{
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public DbSet<EventDefinition> Events { get; set; }
|
||||
public DbSet<Student> Students { get; set; }
|
||||
public DbSet<Team> Teams { get; set; }
|
||||
public DbSet<StudentEventRanking> StudentEventRanking { get; set; }
|
||||
public DbSet<EventOccurrence> EventOccurrences { get; set; }
|
||||
public DbSet<EventDefinition> Events { get; set; }
|
||||
public DbSet<Student> Students { get; set; }
|
||||
public DbSet<Team> Teams { get; set; }
|
||||
public DbSet<StudentEventRanking> StudentEventRanking { get; set; }
|
||||
public DbSet<EventOccurrence> EventOccurrences { get; set; }
|
||||
public DbSet<Career> Careers { get; set; }
|
||||
|
||||
public AppDbContext()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Core.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Data.Configurations
|
||||
{
|
||||
public class CareerConfiguration : IEntityTypeConfiguration<Career>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Career> builder)
|
||||
{
|
||||
builder.HasKey(c => c.Id);
|
||||
|
||||
// Indexes
|
||||
builder.HasIndex(c => c.Name).IsUnique();
|
||||
|
||||
// Constraints
|
||||
builder.Property(c => c.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,18 @@ namespace Data.Configurations
|
||||
builder.Property(e => e.Documentation)
|
||||
.HasMaxLength(500);
|
||||
|
||||
// Value conversions for enums
|
||||
builder.Property(e => e.EventFormat)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(50);
|
||||
}
|
||||
}
|
||||
// Value conversions for enums
|
||||
builder.Property(e => e.EventFormat)
|
||||
.HasConversion<string>()
|
||||
.HasMaxLength(50);
|
||||
|
||||
// Ignore RelatedCareersText (not mapped to database)
|
||||
builder.Ignore(e => e.RelatedCareersText);
|
||||
|
||||
// Many-to-many relationship with Career
|
||||
builder.HasMany(e => e.RelatedCareers)
|
||||
.WithMany()
|
||||
.UsingEntity(j => j.ToTable("EventDefinitionCareers"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20251228200039_RelatedCareers")]
|
||||
partial class RelatedCareers
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.8");
|
||||
|
||||
modelBuilder.Entity("CareerEventDefinition", b =>
|
||||
{
|
||||
b.Property<int>("EventDefinitionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RelatedCareersId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("EventDefinitionId", "RelatedCareersId");
|
||||
|
||||
b.HasIndex("RelatedCareersId");
|
||||
|
||||
b.ToTable("EventDefinitionCareers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.Career", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Careers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.EventDefinition", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterEligibilityCountRegionals")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("ChapterEligibilityCountState")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(1000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Documentation")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Eligibility")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EventFormat")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("LevelOfEffort")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MaxTeamSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("MinTeamSize")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("OnSiteActivity")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<bool>("Presubmission")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SemifinalistActivity")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ShortName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Theme")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventFormat");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Events");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.EventOccurrence", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Date")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("EndTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("EventDefinitionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SpecialEventType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("StartTime")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Time")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("EventDefinitionId");
|
||||
|
||||
b.HasIndex("SpecialEventType");
|
||||
|
||||
b.HasIndex("StartTime");
|
||||
|
||||
b.ToTable("EventOccurrences");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.Student", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Grade")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("NationalId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("OfficerRole")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RegionalId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("StateId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TsaYear")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Email");
|
||||
|
||||
b.HasIndex("Grade");
|
||||
|
||||
b.HasIndex("FirstName", "LastName");
|
||||
|
||||
b.ToTable("Students");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.StudentEventRanking", b =>
|
||||
{
|
||||
b.Property<int>("StudentId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EventDefinitionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("Rank")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("StudentId", "EventDefinitionId");
|
||||
|
||||
b.HasIndex("EventDefinitionId");
|
||||
|
||||
b.HasIndex("Rank");
|
||||
|
||||
b.HasIndex("StudentId");
|
||||
|
||||
b.ToTable("StudentEventRanking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.Team", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int?>("CaptainId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("EventId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Identifier")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CaptainId");
|
||||
|
||||
b.HasIndex("EventId");
|
||||
|
||||
b.HasIndex("EventId", "Identifier");
|
||||
|
||||
b.ToTable("Teams");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StudentTeam", b =>
|
||||
{
|
||||
b.Property<int>("StudentsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("TeamsId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("StudentsId", "TeamsId");
|
||||
|
||||
b.HasIndex("TeamsId");
|
||||
|
||||
b.ToTable("TeamStudents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CareerEventDefinition", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.EventDefinition", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("EventDefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Core.Entities.Career", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RelatedCareersId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.EventOccurrence", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.EventDefinition", "EventDefinition")
|
||||
.WithMany()
|
||||
.HasForeignKey("EventDefinitionId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("EventDefinition");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.StudentEventRanking", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.EventDefinition", "EventDefinition")
|
||||
.WithMany()
|
||||
.HasForeignKey("EventDefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Core.Entities.Student", "Student")
|
||||
.WithMany("EventRankings")
|
||||
.HasForeignKey("StudentId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("EventDefinition");
|
||||
|
||||
b.Navigation("Student");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.Team", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.Student", "Captain")
|
||||
.WithMany()
|
||||
.HasForeignKey("CaptainId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.HasOne("Core.Entities.EventDefinition", "Event")
|
||||
.WithMany()
|
||||
.HasForeignKey("EventId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Captain");
|
||||
|
||||
b.Navigation("Event");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("StudentTeam", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.Student", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("StudentsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Core.Entities.Team", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TeamsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.Student", b =>
|
||||
{
|
||||
b.Navigation("EventRankings");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RelatedCareers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Careers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
Name = table.Column<string>(type: "TEXT", maxLength: 200, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Careers", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "EventDefinitionCareers",
|
||||
columns: table => new
|
||||
{
|
||||
EventDefinitionId = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
RelatedCareersId = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_EventDefinitionCareers", x => new { x.EventDefinitionId, x.RelatedCareersId });
|
||||
table.ForeignKey(
|
||||
name: "FK_EventDefinitionCareers_Careers_RelatedCareersId",
|
||||
column: x => x.RelatedCareersId,
|
||||
principalTable: "Careers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_EventDefinitionCareers_Events_EventDefinitionId",
|
||||
column: x => x.EventDefinitionId,
|
||||
principalTable: "Events",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Careers_Name",
|
||||
table: "Careers",
|
||||
column: "Name",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EventDefinitionCareers_RelatedCareersId",
|
||||
table: "EventDefinitionCareers",
|
||||
column: "RelatedCareersId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "EventDefinitionCareers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Careers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,40 @@ namespace Data.Migrations
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "9.0.8");
|
||||
|
||||
modelBuilder.Entity("CareerEventDefinition", b =>
|
||||
{
|
||||
b.Property<int>("EventDefinitionId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<int>("RelatedCareersId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("EventDefinitionId", "RelatedCareersId");
|
||||
|
||||
b.HasIndex("RelatedCareersId");
|
||||
|
||||
b.ToTable("EventDefinitionCareers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.Career", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Name")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Careers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.EventDefinition", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -264,6 +298,21 @@ namespace Data.Migrations
|
||||
b.ToTable("TeamStudents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("CareerEventDefinition", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.EventDefinition", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("EventDefinitionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Core.Entities.Career", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RelatedCareersId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Core.Entities.EventOccurrence", b =>
|
||||
{
|
||||
b.HasOne("Core.Entities.EventDefinition", "EventDefinition")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
@page "/events/create"
|
||||
@attribute [Authorize]
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Utility
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@inject AppDbContext context
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@@ -52,6 +54,9 @@
|
||||
<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>
|
||||
|
||||
@@ -107,15 +112,66 @@
|
||||
|
||||
private FormChangeTracker? _formChangeTracker;
|
||||
|
||||
private void OnValidSubmit()
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
_formChangeTracker?.AllowNavigation();
|
||||
|
||||
// Normalize and process related careers
|
||||
await ProcessRelatedCareersAsync(EventDefinition);
|
||||
|
||||
context.Events.Add(EventDefinition);
|
||||
context.SaveChanges();
|
||||
await context.SaveChangesAsync();
|
||||
NavigationManager.NavigateTo(ReturnUrl ?? "/events");
|
||||
}
|
||||
|
||||
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 void HandleCancel()
|
||||
{
|
||||
_formChangeTracker?.AllowNavigation();
|
||||
|
||||
@@ -102,6 +102,22 @@
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">Notes</MudText>
|
||||
<MudText Typo="Typo.body1">@eventdefinition.Notes</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.subtitle2" Class="mud-text-secondary">Related Careers</MudText>
|
||||
@if (eventdefinition.RelatedCareers?.Any() == true)
|
||||
{
|
||||
<ul style="margin-top: 8px; padding-left: 20px;">
|
||||
@foreach (var career in eventdefinition.RelatedCareers.OrderBy(c => c.Name))
|
||||
{
|
||||
<li style="margin-bottom: 4px;"><MudText Typo="Typo.body1">@career.Name</MudText></li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body1" Class="mud-text-secondary">None</MudText>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
|
||||
@@ -116,7 +132,9 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
eventdefinition = await context.Events.FirstOrDefaultAsync(m => m.Id == Id);
|
||||
eventdefinition = await context.Events
|
||||
.Include(e => e.RelatedCareers)
|
||||
.FirstOrDefaultAsync(m => m.Id == Id);
|
||||
|
||||
if (eventdefinition is null)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
@attribute [Authorize]
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using WebApp.Components.Shared.Components
|
||||
@using Core.Utility
|
||||
@inject AppDbContext context
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@@ -59,6 +60,9 @@
|
||||
<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>
|
||||
|
||||
@@ -119,23 +123,45 @@
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
EventDefinition ??= await context.Events.FirstOrDefaultAsync(m => m.Id == Id);
|
||||
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 void OnValidSubmit()
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
context.Attach(EventDefinition!).State = EntityState.Modified;
|
||||
// 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
|
||||
{
|
||||
context.SaveChangesAsync();
|
||||
await context.SaveChangesAsync();
|
||||
_formChangeTracker?.AllowNavigation();
|
||||
NavigationManager.NavigateTo(ReturnUrl ?? "/events");
|
||||
}
|
||||
@@ -152,6 +178,54 @@
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user