using Lagerbestand.Models; using Microsoft.EntityFrameworkCore; namespace Lagerbestand.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } public DbSet CatalogItems => Set(); public DbSet InventoryItems => Set(); public DbSet Users => Set(); public DbSet Customers => Set(); public DbSet Vehicles => Set(); public DbSet StorageLocations => Set(); public DbSet TireStacks => Set(); public DbSet TireItems => Set(); public DbSet TireInspections => Set(); public DbSet SiteSettings => Set(); // Kanban entities public DbSet KanbanColumns => Set(); public DbSet KanbanLabels => Set(); public DbSet KanbanTasks => Set(); public DbSet KanbanTaskTodos => Set(); public DbSet KanbanTaskComments => Set(); public DbSet KanbanTaskNotes => Set(); public DbSet KanbanTaskAttachments => Set(); public DbSet KanbanTaskTemplates => Set(); public DbSet KanbanTaskTemplateTodos => Set(); public DbSet KanbanTaskLabels => Set(); public DbSet KanbanTaskAssignees => Set(); public DbSet KanbanTaskCustomers => Set(); public DbSet KanbanTaskTireStacks => Set(); public DbSet KanbanTaskTemplateLabels => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // CatalogItem configuration modelBuilder.Entity(entity => { entity.ToTable("CatalogItems"); entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(200); entity.Property(e => e.PictureFileName).HasMaxLength(500); entity.Property(e => e.TireSize).HasMaxLength(50); entity.Property(e => e.Season).HasConversion(); entity.Property(e => e.Type).HasConversion().HasDefaultValue(CatalogItemType.Reifen); }); // InventoryItem configuration (legacy) modelBuilder.Entity(entity => { entity.ToTable("InventoryItems"); entity.HasKey(e => e.Id); entity.Property(e => e.QrCode).IsRequired().HasMaxLength(500); entity.Property(e => e.Name).HasMaxLength(200); entity.Property(e => e.Description).HasMaxLength(500); entity.HasIndex(e => e.QrCode); entity.HasOne(e => e.CatalogItem) .WithMany() .HasForeignKey(e => e.CatalogItemId) .OnDelete(DeleteBehavior.SetNull); }); // AppUser configuration modelBuilder.Entity(entity => { entity.ToTable("Users"); entity.HasKey(e => e.Id); entity.Property(e => e.Username).IsRequired().HasMaxLength(100); entity.Property(e => e.PasswordHash).IsRequired().HasMaxLength(256); entity.Property(e => e.DisplayName).HasMaxLength(200); entity.Property(e => e.Role).HasConversion(); entity.HasIndex(e => e.Username).IsUnique(); entity.Property(e => e.TwoFactorSecret).HasMaxLength(256); }); // Customer configuration modelBuilder.Entity(entity => { entity.ToTable("Customers"); entity.HasKey(e => e.Id); entity.Property(e => e.FirstName).HasMaxLength(100); entity.Property(e => e.LastName).IsRequired().HasMaxLength(100); entity.Property(e => e.Company).HasMaxLength(200); entity.Property(e => e.Phone).HasMaxLength(20); entity.Property(e => e.Email).HasMaxLength(100); entity.Property(e => e.Notes).HasMaxLength(500); entity.Property(e => e.CustomerNumber).IsRequired().HasMaxLength(20); entity.HasIndex(e => e.CustomerNumber).IsUnique(); entity.HasIndex(e => new { e.LastName, e.FirstName }); }); // Vehicle configuration modelBuilder.Entity(entity => { entity.ToTable("Vehicles"); entity.HasKey(e => e.Id); entity.Property(e => e.Make).IsRequired().HasMaxLength(100); entity.Property(e => e.Model).HasMaxLength(100); entity.Property(e => e.TireSize).IsRequired().HasMaxLength(50); entity.Property(e => e.Notes).HasMaxLength(500); entity.HasIndex(e => e.CustomerId); entity.HasIndex(e => new { e.Make, e.Model }); entity.HasIndex(e => new { e.CustomerId, e.IsPrimary }); entity.HasOne(e => e.Customer) .WithMany(c => c.Vehicles) .HasForeignKey(e => e.CustomerId) .OnDelete(DeleteBehavior.Cascade); }); // StorageLocation configuration - ABCD code format modelBuilder.Entity(entity => { entity.ToTable("StorageLocations"); entity.HasKey(e => e.Id); // Code is the 4-digit ABCD format (e.g., "2235") entity.Property(e => e.Code).IsRequired().HasMaxLength(4); // Area enum stored as int (1=Werkstatt, 2=Lager) entity.Property(e => e.Area).HasConversion(); // Row is an int (Werkstatt: 1-3, Lager: 1-5) entity.Property(e => e.Row).IsRequired(); // Position enum stored as int (1=links oben ... 4=rechts unten) entity.Property(e => e.Position).HasConversion().IsRequired(); // SlotNumber is int (1-9) entity.Property(e => e.SlotNumber).IsRequired(); // Concurrency token: prevents double-booking when two requests // try to occupy/clear the same location simultaneously entity.Property(e => e.IsOccupied).IsConcurrencyToken(); entity.Property(e => e.Notes).HasMaxLength(500); // Indexes entity.HasIndex(e => e.Code).IsUnique(); entity.HasIndex(e => new { e.Area, e.Row }); entity.HasIndex(e => new { e.Area, e.Row, e.Position, e.SlotNumber }).IsUnique(); entity.HasIndex(e => e.IsOccupied); entity.HasIndex(e => e.TireStackId); // Relationship to the occupied TireStack entity.HasOne(e => e.OccupiedTireStack) .WithOne() .HasForeignKey(e => e.TireStackId) .OnDelete(DeleteBehavior.SetNull); }); // TireStack configuration modelBuilder.Entity(entity => { entity.ToTable("TireStacks"); entity.HasKey(e => e.Id); entity.Property(e => e.TicketNumber).IsRequired().HasMaxLength(50); entity.Property(e => e.TireSize).HasMaxLength(50); entity.Property(e => e.TireBrand).HasMaxLength(100); entity.Property(e => e.TireModel).HasMaxLength(100); entity.Property(e => e.DotCode).HasMaxLength(20); entity.Property(e => e.RimSize).HasMaxLength(20); entity.Property(e => e.Notes).HasMaxLength(1000); entity.Property(e => e.Status).HasConversion(); entity.Property(e => e.Season).HasConversion(); entity.Property(e => e.Condition).HasConversion(); entity.Property(e => e.TreadDepth).HasPrecision(4, 1); entity.Property(e => e.ItemCount).HasDefaultValue(TireStack.TiresPerStack); // CustomerId is optional (nullable) for inventory items without a customer entity.Property(e => e.CustomerId).IsRequired(false); entity.Property(e => e.VehicleId).IsRequired(false); entity.HasIndex(e => e.TicketNumber).IsUnique(); entity.HasIndex(e => e.Status); entity.HasIndex(e => e.CustomerId); entity.HasIndex(e => e.VehicleId); entity.HasIndex(e => e.StorageLocationId); // Customer relationship - optional for inventory items entity.HasOne(e => e.Customer) .WithMany(c => c.TireStacks) .HasForeignKey(e => e.CustomerId) .IsRequired(false) .OnDelete(DeleteBehavior.Restrict); // Vehicle relationship - optional entity.HasOne(e => e.Vehicle) .WithMany(v => v.TireStacks) .HasForeignKey(e => e.VehicleId) .IsRequired(false) .OnDelete(DeleteBehavior.SetNull); entity.HasOne(e => e.StorageLocation) .WithMany(s => s.TireStacks) .HasForeignKey(e => e.StorageLocationId) .OnDelete(DeleteBehavior.SetNull); entity.HasOne(e => e.CatalogItem) .WithMany() .HasForeignKey(e => e.CatalogItemId) .OnDelete(DeleteBehavior.SetNull); }); // TireItem configuration modelBuilder.Entity(entity => { entity.ToTable("TireItems"); entity.HasKey(e => e.Id); entity.Property(e => e.Position).HasConversion(); entity.Property(e => e.Condition).HasConversion(); entity.Property(e => e.TreadDepth).HasPrecision(4, 1); entity.Property(e => e.DamageNotes).HasMaxLength(500); entity.Property(e => e.DotCode).HasMaxLength(20); entity.Property(e => e.PhotoFileName).HasMaxLength(500); entity.HasIndex(e => e.TireStackId); entity.HasOne(e => e.TireStack) .WithMany() .HasForeignKey(e => e.TireStackId) .OnDelete(DeleteBehavior.Cascade); }); // TireInspection configuration modelBuilder.Entity(entity => { entity.ToTable("TireInspections"); entity.HasKey(e => e.Id); entity.Property(e => e.Source).HasMaxLength(50); entity.Property(e => e.RecordSn).HasMaxLength(100); entity.Property(e => e.ReportUrl).HasMaxLength(500); entity.Property(e => e.LicensePlate).HasMaxLength(20); entity.Property(e => e.Recommendation).HasMaxLength(1000); entity.Property(e => e.RawJson).HasColumnType("LONGTEXT"); entity.Property(e => e.RatingFL).HasConversion(); entity.Property(e => e.RatingFR).HasConversion(); entity.Property(e => e.RatingRL).HasConversion(); entity.Property(e => e.RatingRR).HasConversion(); entity.HasIndex(e => e.TireStackId); entity.HasIndex(e => e.RecordSn); entity.HasOne(e => e.TireStack) .WithMany(t => t.Inspections) .HasForeignKey(e => e.TireStackId) .OnDelete(DeleteBehavior.Cascade); }); // SiteSettings configuration modelBuilder.Entity(entity => { entity.ToTable("SiteSettings"); entity.HasKey(e => e.Id); entity.Property(e => e.Key).IsRequired().HasMaxLength(100); entity.Property(e => e.Value).IsRequired().HasMaxLength(4000); entity.HasIndex(e => e.Key).IsUnique(); }); // ======================================== // Kanban Board Configurations // ======================================== // KanbanColumn configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanColumns"); entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(100); entity.Property(e => e.Color).HasMaxLength(7); entity.Property(e => e.Icon).HasMaxLength(10); entity.HasIndex(e => e.SortOrder); }); // KanbanLabel configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanLabels"); entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(50); entity.Property(e => e.Color).IsRequired().HasMaxLength(7); entity.Property(e => e.Description).HasMaxLength(200); entity.HasIndex(e => e.SortOrder); }); // KanbanTask configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTasks"); entity.HasKey(e => e.Id); entity.Property(e => e.Title).IsRequired().HasMaxLength(200); entity.Property(e => e.Priority).HasConversion(); entity.HasIndex(e => e.ColumnId); entity.HasIndex(e => e.DueDate); entity.HasIndex(e => e.IsArchived); entity.HasIndex(e => new { e.ColumnId, e.SortOrder }); entity.HasOne(e => e.Column) .WithMany(c => c.Tasks) .HasForeignKey(e => e.ColumnId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.CreatedByUser) .WithMany() .HasForeignKey(e => e.CreatedByUserId) .OnDelete(DeleteBehavior.SetNull); }); // KanbanTaskTodo configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskTodos"); entity.HasKey(e => e.Id); entity.Property(e => e.Text).IsRequired().HasMaxLength(500); entity.HasIndex(e => e.TaskId); entity.HasOne(e => e.Task) .WithMany(t => t.Todos) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); }); // KanbanTaskComment configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskComments"); entity.HasKey(e => e.Id); entity.Property(e => e.Content).IsRequired(); entity.Property(e => e.CommentType).HasConversion(); entity.HasIndex(e => e.TaskId); entity.HasIndex(e => e.CreatedAt); entity.HasOne(e => e.Task) .WithMany(t => t.Comments) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.SetNull); }); // KanbanTaskNote configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskNotes"); entity.HasKey(e => e.Id); entity.Property(e => e.Title).IsRequired().HasMaxLength(100); entity.HasIndex(e => e.TaskId); entity.HasOne(e => e.Task) .WithMany(t => t.Notes) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.LastEditedByUser) .WithMany() .HasForeignKey(e => e.LastEditedByUserId) .OnDelete(DeleteBehavior.SetNull); }); // KanbanTaskAttachment configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskAttachments"); entity.HasKey(e => e.Id); entity.Property(e => e.FileName).IsRequired().HasMaxLength(255); entity.Property(e => e.ContentType).IsRequired().HasMaxLength(100); entity.Property(e => e.FilePath).HasMaxLength(500); // FileContent is nullable for migration to file-based storage entity.Property(e => e.FileContent).IsRequired(false); entity.HasIndex(e => e.TaskId); entity.HasOne(e => e.Task) .WithMany(t => t.Attachments) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.UploadedByUser) .WithMany() .HasForeignKey(e => e.UploadedByUserId) .OnDelete(DeleteBehavior.SetNull); }); // KanbanTaskTemplate configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskTemplates"); entity.HasKey(e => e.Id); entity.Property(e => e.Name).IsRequired().HasMaxLength(100); entity.Property(e => e.Description).HasMaxLength(500); entity.Property(e => e.TitlePattern).HasMaxLength(200); entity.Property(e => e.DefaultPriority).HasConversion(); entity.HasIndex(e => e.SortOrder); entity.HasOne(e => e.DefaultColumn) .WithMany() .HasForeignKey(e => e.DefaultColumnId) .OnDelete(DeleteBehavior.SetNull); }); // KanbanTaskTemplateTodo configuration modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskTemplateTodos"); entity.HasKey(e => e.Id); entity.Property(e => e.Text).IsRequired().HasMaxLength(500); entity.HasOne(e => e.Template) .WithMany(t => t.Todos) .HasForeignKey(e => e.TemplateId) .OnDelete(DeleteBehavior.Cascade); }); // KanbanTaskLabel (many-to-many) modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskLabels"); entity.HasKey(e => new { e.TaskId, e.LabelId }); entity.HasOne(e => e.Task) .WithMany(t => t.TaskLabels) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.Label) .WithMany(l => l.TaskLabels) .HasForeignKey(e => e.LabelId) .OnDelete(DeleteBehavior.Cascade); }); // KanbanTaskTemplateLabel (many-to-many) modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskTemplateLabels"); entity.HasKey(e => new { e.TemplateId, e.LabelId }); entity.HasOne(e => e.Template) .WithMany(t => t.TemplateLabels) .HasForeignKey(e => e.TemplateId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.Label) .WithMany(l => l.TemplateLabels) .HasForeignKey(e => e.LabelId) .OnDelete(DeleteBehavior.Cascade); }); // KanbanTaskAssignee (many-to-many) modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskAssignees"); entity.HasKey(e => new { e.TaskId, e.UserId }); entity.HasOne(e => e.Task) .WithMany(t => t.Assignees) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.User) .WithMany() .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.Cascade); }); // KanbanTaskCustomer (many-to-many) modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskCustomers"); entity.HasKey(e => new { e.TaskId, e.CustomerId }); entity.HasOne(e => e.Task) .WithMany(t => t.TaskCustomers) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.Customer) .WithMany() .HasForeignKey(e => e.CustomerId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.Vehicle) .WithMany() .HasForeignKey(e => e.VehicleId) .OnDelete(DeleteBehavior.SetNull); }); // KanbanTaskTireStack (many-to-many) modelBuilder.Entity(entity => { entity.ToTable("KanbanTaskTireStacks"); entity.HasKey(e => new { e.TaskId, e.TireStackId }); entity.HasOne(e => e.Task) .WithMany(t => t.TaskTireStacks) .HasForeignKey(e => e.TaskId) .OnDelete(DeleteBehavior.Cascade); entity.HasOne(e => e.TireStack) .WithMany() .HasForeignKey(e => e.TireStackId) .OnDelete(DeleteBehavior.Cascade); }); } }