diff --git a/Core/Entities/EventOccurrence.cs b/Core/Entities/EventOccurrence.cs index c171b18..1bf82d3 100644 --- a/Core/Entities/EventOccurrence.cs +++ b/Core/Entities/EventOccurrence.cs @@ -1,14 +1,22 @@ - -namespace Core.Entities +namespace Core.Entities { public class EventOccurrence { + public int Id { get; set; } + + public int? EventDefinitionId { get; set; } + + public string? SpecialEventType { get; set; } + public string Name { get; set; } = null!; public string Time { get; set; } = null!; public string Date { get; set; } = null!; public DateTime StartTime { get; set; } public DateTime? EndTime { get; set; } - public string Location { get; set; } = null!; + public string? Location { get; set; } + + // Navigation property + public EventDefinition? EventDefinition { get; set; } public bool SignupSubmitPickup => Name.Contains("Sign-up") || diff --git a/Data/AppDbContext.cs b/Data/AppDbContext.cs index f6ca913..bf52323 100644 --- a/Data/AppDbContext.cs +++ b/Data/AppDbContext.cs @@ -10,6 +10,7 @@ namespace Data public DbSet Students { get; set; } public DbSet Teams { get; set; } public DbSet StudentEventRanking { get; set; } + public DbSet EventOccurrences { get; set; } public AppDbContext() { diff --git a/Data/Configurations/EventOccurrenceConfiguration.cs b/Data/Configurations/EventOccurrenceConfiguration.cs new file mode 100644 index 0000000..ceaa5ad --- /dev/null +++ b/Data/Configurations/EventOccurrenceConfiguration.cs @@ -0,0 +1,48 @@ +using Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Data.Configurations +{ + public class EventOccurrenceConfiguration : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(e => e.Id); + + // Indexes + builder.HasIndex(e => e.StartTime); + builder.HasIndex(e => e.EventDefinitionId); + builder.HasIndex(e => e.SpecialEventType); + + // Foreign key relationship (optional) + builder.HasOne(e => e.EventDefinition) + .WithMany() + .HasForeignKey(e => e.EventDefinitionId) + .OnDelete(DeleteBehavior.Restrict); // Don't cascade delete if EventDefinition is deleted + + // Constraints + builder.Property(e => e.Name) + .IsRequired() + .HasMaxLength(200); + + builder.Property(e => e.Time) + .IsRequired() + .HasMaxLength(100); + + builder.Property(e => e.Date) + .IsRequired() + .HasMaxLength(100); + + builder.Property(e => e.Location) + .HasMaxLength(500); + + builder.Property(e => e.SpecialEventType) + .HasMaxLength(50); + + // Validation: Either EventDefinitionId OR SpecialEventType must be set (enforced at application level) + // EF Core doesn't support mutually exclusive constraints directly, so we'll handle this in validation attributes or service layer + } + } +} + diff --git a/Data/Migrations/20251227205816_EventOccurrence.Designer.cs b/Data/Migrations/20251227205816_EventOccurrence.Designer.cs new file mode 100644 index 0000000..e382738 --- /dev/null +++ b/Data/Migrations/20251227205816_EventOccurrence.Designer.cs @@ -0,0 +1,339 @@ +// +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("20251227205816_EventOccurrence")] + partial class EventOccurrence + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("Core.Entities.EventDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChapterEligibilityCountRegionals") + .HasColumnType("INTEGER"); + + b.Property("ChapterEligibilityCountState") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("TEXT"); + + b.Property("Documentation") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Eligibility") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("EventFormat") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LevelOfEffort") + .HasColumnType("INTEGER"); + + b.Property("MaxTeamSize") + .HasColumnType("INTEGER"); + + b.Property("MinTeamSize") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("OnSiteActivity") + .HasColumnType("INTEGER"); + + b.Property("Presubmission") + .HasColumnType("INTEGER"); + + b.Property("SemifinalistActivity") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("ShortName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventDefinitionId") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SpecialEventType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Grade") + .HasColumnType("INTEGER"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("NationalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OfficerRole") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("RegionalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StateId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("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("StudentId") + .HasColumnType("INTEGER"); + + b.Property("EventDefinitionId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CaptainId") + .HasColumnType("INTEGER"); + + b.Property("EventId") + .HasColumnType("INTEGER"); + + b.Property("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("StudentsId") + .HasColumnType("INTEGER"); + + b.Property("TeamsId") + .HasColumnType("INTEGER"); + + b.HasKey("StudentsId", "TeamsId"); + + b.HasIndex("TeamsId"); + + b.ToTable("TeamStudents", (string)null); + }); + + 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 + } + } +} diff --git a/Data/Migrations/20251227205816_EventOccurrence.cs b/Data/Migrations/20251227205816_EventOccurrence.cs new file mode 100644 index 0000000..5767f69 --- /dev/null +++ b/Data/Migrations/20251227205816_EventOccurrence.cs @@ -0,0 +1,63 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Data.Migrations +{ + /// + public partial class EventOccurrence : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EventOccurrences", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + EventDefinitionId = table.Column(type: "INTEGER", nullable: true), + SpecialEventType = table.Column(type: "TEXT", maxLength: 50, nullable: true), + Name = table.Column(type: "TEXT", maxLength: 200, nullable: false), + Time = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Date = table.Column(type: "TEXT", maxLength: 100, nullable: false), + StartTime = table.Column(type: "TEXT", nullable: false), + EndTime = table.Column(type: "TEXT", nullable: true), + Location = table.Column(type: "TEXT", maxLength: 500, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_EventOccurrences", x => x.Id); + table.ForeignKey( + name: "FK_EventOccurrences_Events_EventDefinitionId", + column: x => x.EventDefinitionId, + principalTable: "Events", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_EventOccurrences_EventDefinitionId", + table: "EventOccurrences", + column: "EventDefinitionId"); + + migrationBuilder.CreateIndex( + name: "IX_EventOccurrences_SpecialEventType", + table: "EventOccurrences", + column: "SpecialEventType"); + + migrationBuilder.CreateIndex( + name: "IX_EventOccurrences_StartTime", + table: "EventOccurrences", + column: "StartTime"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EventOccurrences"); + } + } +} diff --git a/Data/Migrations/AppDbContextModelSnapshot.cs b/Data/Migrations/AppDbContextModelSnapshot.cs index e3c7d3b..9c3ecfb 100644 --- a/Data/Migrations/AppDbContextModelSnapshot.cs +++ b/Data/Migrations/AppDbContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -93,6 +94,55 @@ namespace Data.Migrations b.ToTable("Events"); }); + modelBuilder.Entity("Core.Entities.EventOccurrence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Date") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("EndTime") + .HasColumnType("TEXT"); + + b.Property("EventDefinitionId") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SpecialEventType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StartTime") + .HasColumnType("TEXT"); + + b.Property("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("Id") @@ -214,6 +264,16 @@ namespace Data.Migrations b.ToTable("TeamStudents", (string)null); }); + 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")