Files
ReifenSpaeth/Lagerbestand/Data/AppDbContext.cs
T

523 lines
21 KiB
C#

using Lagerbestand.Models;
using Microsoft.EntityFrameworkCore;
namespace Lagerbestand.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<CatalogItem> CatalogItems => Set<CatalogItem>();
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
public DbSet<AppUser> Users => Set<AppUser>();
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Vehicle> Vehicles => Set<Vehicle>();
public DbSet<StorageLocation> StorageLocations => Set<StorageLocation>();
public DbSet<TireStack> TireStacks => Set<TireStack>();
public DbSet<TireItem> TireItems => Set<TireItem>();
public DbSet<TireInspection> TireInspections => Set<TireInspection>();
public DbSet<SiteSettings> SiteSettings => Set<SiteSettings>();
// Kanban entities
public DbSet<KanbanColumn> KanbanColumns => Set<KanbanColumn>();
public DbSet<KanbanLabel> KanbanLabels => Set<KanbanLabel>();
public DbSet<KanbanTask> KanbanTasks => Set<KanbanTask>();
public DbSet<KanbanTaskTodo> KanbanTaskTodos => Set<KanbanTaskTodo>();
public DbSet<KanbanTaskComment> KanbanTaskComments => Set<KanbanTaskComment>();
public DbSet<KanbanTaskNote> KanbanTaskNotes => Set<KanbanTaskNote>();
public DbSet<KanbanTaskAttachment> KanbanTaskAttachments => Set<KanbanTaskAttachment>();
public DbSet<KanbanTaskTemplate> KanbanTaskTemplates => Set<KanbanTaskTemplate>();
public DbSet<KanbanTaskTemplateTodo> KanbanTaskTemplateTodos => Set<KanbanTaskTemplateTodo>();
public DbSet<KanbanTaskLabel> KanbanTaskLabels => Set<KanbanTaskLabel>();
public DbSet<KanbanTaskAssignee> KanbanTaskAssignees => Set<KanbanTaskAssignee>();
public DbSet<KanbanTaskCustomer> KanbanTaskCustomers => Set<KanbanTaskCustomer>();
public DbSet<KanbanTaskTireStack> KanbanTaskTireStacks => Set<KanbanTaskTireStack>();
public DbSet<KanbanTaskTemplateLabel> KanbanTaskTemplateLabels => Set<KanbanTaskTemplateLabel>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// CatalogItem configuration
modelBuilder.Entity<CatalogItem>(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<int>();
entity.Property(e => e.Type).HasConversion<int>().HasDefaultValue(CatalogItemType.Reifen);
});
// InventoryItem configuration (legacy)
modelBuilder.Entity<InventoryItem>(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<AppUser>(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<int>();
entity.HasIndex(e => e.Username).IsUnique();
entity.Property(e => e.TwoFactorSecret).HasMaxLength(256);
});
// Customer configuration
modelBuilder.Entity<Customer>(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<Vehicle>(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<StorageLocation>(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<int>();
// 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<int>().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<StorageLocation>(e => e.TireStackId)
.OnDelete(DeleteBehavior.SetNull);
});
// TireStack configuration
modelBuilder.Entity<TireStack>(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<int>();
entity.Property(e => e.Season).HasConversion<int>();
entity.Property(e => e.Condition).HasConversion<int>();
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<TireItem>(entity =>
{
entity.ToTable("TireItems");
entity.HasKey(e => e.Id);
entity.Property(e => e.Position).HasConversion<int>();
entity.Property(e => e.Condition).HasConversion<int>();
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<TireInspection>(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<int>();
entity.Property(e => e.RatingFR).HasConversion<int>();
entity.Property(e => e.RatingRL).HasConversion<int>();
entity.Property(e => e.RatingRR).HasConversion<int>();
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<SiteSettings>(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<KanbanColumn>(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<KanbanLabel>(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<KanbanTask>(entity =>
{
entity.ToTable("KanbanTasks");
entity.HasKey(e => e.Id);
entity.Property(e => e.Title).IsRequired().HasMaxLength(200);
entity.Property(e => e.Priority).HasConversion<int>();
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<KanbanTaskTodo>(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<KanbanTaskComment>(entity =>
{
entity.ToTable("KanbanTaskComments");
entity.HasKey(e => e.Id);
entity.Property(e => e.Content).IsRequired();
entity.Property(e => e.CommentType).HasConversion<int>();
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<KanbanTaskNote>(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<KanbanTaskAttachment>(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<KanbanTaskTemplate>(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<int>();
entity.HasIndex(e => e.SortOrder);
entity.HasOne(e => e.DefaultColumn)
.WithMany()
.HasForeignKey(e => e.DefaultColumnId)
.OnDelete(DeleteBehavior.SetNull);
});
// KanbanTaskTemplateTodo configuration
modelBuilder.Entity<KanbanTaskTemplateTodo>(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<KanbanTaskLabel>(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<KanbanTaskTemplateLabel>(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<KanbanTaskAssignee>(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<KanbanTaskCustomer>(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<KanbanTaskTireStack>(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);
});
}
}