326 lines
16 KiB
C#
326 lines
16 KiB
C#
using GerbilManagerWebAPI.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
public class ApplicationContext : DbContext
|
|
{
|
|
public ApplicationContext(DbContextOptions options) : base(options)
|
|
{
|
|
}
|
|
|
|
public DbSet<Gerbil> Gerbils => Set<Gerbil>();
|
|
public DbSet<Litter> Litters => Set<Litter>();
|
|
public DbSet<Contact> Contacts => Set<Contact>();
|
|
public DbSet<Enclosure> Enclosures => Set<Enclosure>();
|
|
public DbSet<ColorVariety> ColorVarieties => Set<ColorVariety>();
|
|
public DbSet<GerbilPhoto> GerbilPhotos => Set<GerbilPhoto>();
|
|
public DbSet<HealthRecord> HealthRecords => Set<HealthRecord>();
|
|
public DbSet<WeightRecord> WeightRecords => Set<WeightRecord>();
|
|
public DbSet<SaleContract> SaleContracts => Set<SaleContract>();
|
|
public DbSet<BreederSettings> BreederSettings => Set<BreederSettings>();
|
|
public DbSet<Site> Sites => Set<Site>();
|
|
public DbSet<Page> Pages => Set<Page>();
|
|
public DbSet<Block> Blocks => Set<Block>();
|
|
public DbSet<Media> Media => Set<Media>();
|
|
public DbSet<Request> Requests => Set<Request>();
|
|
public DbSet<MailSettings> MailSettings => Set<MailSettings>();
|
|
|
|
// Keep Gerbil.NameSearch in sync on every save (separator-insensitive search key),
|
|
// so it can never drift from Name regardless of which code path mutates the entity.
|
|
public override int SaveChanges(bool acceptAllChangesOnSuccess)
|
|
{
|
|
SyncNameSearch();
|
|
return base.SaveChanges(acceptAllChangesOnSuccess);
|
|
}
|
|
|
|
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
|
{
|
|
SyncNameSearch();
|
|
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
|
}
|
|
|
|
private void SyncNameSearch()
|
|
{
|
|
foreach (var entry in ChangeTracker.Entries<Gerbil>())
|
|
{
|
|
if (entry.State is EntityState.Added or EntityState.Modified)
|
|
entry.Entity.NameSearch = GerbilSearch.Normalize(entry.Entity.Name);
|
|
}
|
|
}
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.Entity<Gerbil>(e =>
|
|
{
|
|
// Enums persisted as their string names (readable, Gridify-friendly).
|
|
e.Property(g => g.Gender).HasConversion<string>();
|
|
e.Property(g => g.Status).HasConversion<string>();
|
|
|
|
// Residency defaults to true (own stock unless explicitly marked external).
|
|
e.Property(g => g.IsResident).HasDefaultValue(true);
|
|
|
|
// FEAT-14: character traits stored as a JSON text column (works on both
|
|
// Npgsql and the SQLite test host; opaque labels, no backend vocabulary).
|
|
var traitsConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<string>, string>(
|
|
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
|
|
v => string.IsNullOrEmpty(v)
|
|
? new List<string>()
|
|
: System.Text.Json.JsonSerializer.Deserialize<List<string>>(v, (System.Text.Json.JsonSerializerOptions?)null) ?? new List<string>());
|
|
var traitsComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer<List<string>>(
|
|
(a, b) => (a ?? new List<string>()).SequenceEqual(b ?? new List<string>()),
|
|
v => v == null ? 0 : v.Aggregate(0, (h, s) => HashCode.Combine(h, s.GetHashCode())),
|
|
v => v.ToList());
|
|
e.Property(g => g.CharacterTraits).HasConversion(traitsConverter, traitsComparer);
|
|
|
|
e.HasOne(g => g.Litter).WithMany()
|
|
.HasForeignKey(g => g.LitterId).OnDelete(DeleteBehavior.SetNull);
|
|
e.HasOne(g => g.OriginContact).WithMany()
|
|
.HasForeignKey(g => g.OriginContactId).OnDelete(DeleteBehavior.Restrict);
|
|
e.HasOne(g => g.ReceiverContact).WithMany()
|
|
.HasForeignKey(g => g.ReceiverContactId).OnDelete(DeleteBehavior.Restrict);
|
|
e.HasOne(g => g.Enclosure).WithMany(en => en.Gerbils)
|
|
.HasForeignKey(g => g.EnclosureId).OnDelete(DeleteBehavior.SetNull);
|
|
e.HasOne(g => g.ColorVariety).WithMany()
|
|
.HasForeignKey(g => g.ColorVarietyId).OnDelete(DeleteBehavior.SetNull);
|
|
});
|
|
|
|
modelBuilder.Entity<Litter>(e =>
|
|
{
|
|
e.HasOne(l => l.Father).WithMany()
|
|
.HasForeignKey(l => l.FatherId).OnDelete(DeleteBehavior.Restrict);
|
|
e.HasOne(l => l.Mother).WithMany()
|
|
.HasForeignKey(l => l.MotherId).OnDelete(DeleteBehavior.Restrict);
|
|
});
|
|
|
|
modelBuilder.Entity<HealthRecord>(e =>
|
|
{
|
|
e.Property(h => h.Type).HasConversion<string>();
|
|
e.HasOne<Gerbil>().WithMany()
|
|
.HasForeignKey(h => h.GerbilId).OnDelete(DeleteBehavior.Cascade);
|
|
});
|
|
|
|
modelBuilder.Entity<WeightRecord>(e =>
|
|
e.HasOne<Gerbil>().WithMany()
|
|
.HasForeignKey(w => w.GerbilId).OnDelete(DeleteBehavior.Cascade));
|
|
|
|
modelBuilder.Entity<GerbilPhoto>(e =>
|
|
e.HasOne<Gerbil>().WithMany()
|
|
.HasForeignKey(p => p.GerbilId).OnDelete(DeleteBehavior.Cascade));
|
|
|
|
// FEAT-13: Abgabeverträge + Zuchtprofil.
|
|
modelBuilder.Entity<SaleContract>(e =>
|
|
{
|
|
e.Property(c => c.Price).HasPrecision(10, 2);
|
|
// Restrict: ein Kontakt mit Verträgen ist ein Dokumentenbestand,
|
|
// kein versehentlich löschbarer Datensatz.
|
|
e.HasOne(c => c.Contact).WithMany()
|
|
.HasForeignKey(c => c.ContactId).OnDelete(DeleteBehavior.Restrict);
|
|
});
|
|
|
|
modelBuilder.Entity<SaleContractAnimal>(e =>
|
|
{
|
|
e.HasKey(a => new { a.SaleContractId, a.GerbilId });
|
|
e.HasOne<SaleContract>().WithMany(c => c.Animals)
|
|
.HasForeignKey(a => a.SaleContractId).OnDelete(DeleteBehavior.Cascade);
|
|
// Cascade: wird ein Tier gelöscht, verschwindet nur die Verknüpfung —
|
|
// der Vertrag (und seine .docx als Beleg) bleibt bestehen.
|
|
e.HasOne(a => a.Gerbil).WithMany()
|
|
.HasForeignKey(a => a.GerbilId).OnDelete(DeleteBehavior.Cascade);
|
|
});
|
|
|
|
// Zuchtprofil: genau eine (leere) Zeile mit fixer Id.
|
|
modelBuilder.Entity<BreederSettings>()
|
|
.HasData(new BreederSettings { Id = GerbilManagerWebAPI.Models.BreederSettings.SingletonId });
|
|
|
|
// WEB epic: CMS content model.
|
|
modelBuilder.Entity<Site>(e =>
|
|
{
|
|
var navConverter = new Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<List<Guid>, string>(
|
|
v => System.Text.Json.JsonSerializer.Serialize(v, (System.Text.Json.JsonSerializerOptions?)null),
|
|
v => string.IsNullOrEmpty(v)
|
|
? new List<Guid>()
|
|
: System.Text.Json.JsonSerializer.Deserialize<List<Guid>>(v, (System.Text.Json.JsonSerializerOptions?)null) ?? new List<Guid>());
|
|
var navComparer = new Microsoft.EntityFrameworkCore.ChangeTracking.ValueComparer<List<Guid>>(
|
|
(a, b) => (a ?? new List<Guid>()).SequenceEqual(b ?? new List<Guid>()),
|
|
v => v == null ? 0 : v.Aggregate(0, (h, x) => HashCode.Combine(h, x.GetHashCode())),
|
|
v => v.ToList());
|
|
e.Property(s => s.NavOrder).HasConversion(navConverter, navComparer);
|
|
});
|
|
|
|
modelBuilder.Entity<Page>(e =>
|
|
{
|
|
e.HasIndex(p => p.Slug).IsUnique();
|
|
e.Property(p => p.Status).HasConversion<string>();
|
|
e.HasMany(p => p.Blocks).WithOne()
|
|
.HasForeignKey(b => b.PageId).OnDelete(DeleteBehavior.Cascade);
|
|
});
|
|
|
|
modelBuilder.Entity<Block>(e => e.Property(b => b.Type).HasConversion<string>());
|
|
|
|
SeedCms(modelBuilder);
|
|
|
|
// INBOX epic: Gmail request inbox.
|
|
modelBuilder.Entity<Request>(e =>
|
|
{
|
|
e.Property(r => r.Status).HasConversion<string>();
|
|
e.HasIndex(r => r.GmailMessageId).IsUnique();
|
|
e.HasOne(r => r.AssignedContact).WithMany()
|
|
.HasForeignKey(r => r.AssignedContactId).OnDelete(DeleteBehavior.Restrict);
|
|
});
|
|
// exactly one MailSettings row, fixed id.
|
|
modelBuilder.Entity<MailSettings>()
|
|
.HasData(new MailSettings { Id = GerbilManagerWebAPI.Models.MailSettings.SingletonId });
|
|
|
|
SeedColorVarieties(modelBuilder);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seed the public website's CMS: one Site + the 6 Jimdo-mirror pages, each with a
|
|
/// placeholder Heading; the abgabetiere page also carries an auto AbgabetiereList block
|
|
/// (resolved from live ForSale animals at snapshot time). Hand-editable afterwards.
|
|
/// </summary>
|
|
private static void SeedCms(ModelBuilder modelBuilder)
|
|
{
|
|
Guid Pid(int n) => new($"51720001-0000-0000-0000-{n:D12}");
|
|
Guid Bid(int n) => new($"51720002-0000-0000-0000-{n:D12}");
|
|
|
|
(int n, string Slug, string Title)[] pages =
|
|
{
|
|
(1, "start", "Startseite"),
|
|
(2, "ueber-die-zucht", "Über die Zucht"),
|
|
(3, "abgabetiere", "Abgabetiere"),
|
|
(4, "abgabebedingungen", "Abgabebedingungen"),
|
|
(5, "farben-genetik", "Farben & Genetik"),
|
|
(6, "kontakt", "Kontakt"),
|
|
};
|
|
|
|
var pageRows = new List<Page>();
|
|
var blockRows = new List<Block>();
|
|
foreach (var p in pages)
|
|
{
|
|
pageRows.Add(new Page { Id = Pid(p.n), Slug = p.Slug, Title = p.Title, Status = PageStatus.Published });
|
|
// placeholder Heading per page
|
|
blockRows.Add(new Block
|
|
{
|
|
Id = Bid(p.n), PageId = Pid(p.n), Order = 0, Type = BlockType.Heading,
|
|
Data = $"{{\"text\":\"{p.Title}\",\"level\":1}}",
|
|
});
|
|
}
|
|
// abgabetiere (n=3): auto AbgabetiereList as the second block
|
|
blockRows.Add(new Block
|
|
{
|
|
Id = Bid(10), PageId = Pid(3), Order = 1, Type = BlockType.AbgabetiereList,
|
|
Data = "{\"mode\":\"auto\",\"intro\":\"\"}",
|
|
});
|
|
|
|
modelBuilder.Entity<Page>().HasData(pageRows);
|
|
modelBuilder.Entity<Block>().HasData(blockRows);
|
|
|
|
modelBuilder.Entity<Site>().HasData(new Site
|
|
{
|
|
Id = Site.SingletonId,
|
|
DefaultLocale = "de",
|
|
NavOrder = pages.Select(p => Pid(p.n)).ToList(),
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seed the colour-variety catalog. Source of truth = Kevin's GEN-2 generated list
|
|
/// (gerbil-manager-web/src/genetics/colorVarietySeed.generated.json): 73 varieties
|
|
/// (the 18 frozen-contract names first, then the baseportal.de extension), in stable
|
|
/// SortOrder. Names are UI filter keys — spelling changes route through god.
|
|
/// </summary>
|
|
private static void SeedColorVarieties(ModelBuilder modelBuilder)
|
|
{
|
|
(string Name, string Genotype)[] catalog =
|
|
{
|
|
("Pink Eyed White (PEW)", "AA chch DD EE GG pp spsp rere"),
|
|
("Hermelin", "aa chch DD EE GG PP spsp rere"),
|
|
("Himalaya", "AA chch DD EE GG PP spsp rere"),
|
|
("Zobel", "aa cchmcchm DD EE gg PP spsp rere"),
|
|
("Rotaugenschimmel", "AA CC DD efef GG pp spsp rere"),
|
|
("Agouti", "AA CC DD EE GG PP spsp rere"),
|
|
("Schwarz", "aa CC DD EE GG PP spsp rere"),
|
|
("Silberagouti", "AA CC DD EE gg PP spsp rere"),
|
|
("Anthrazit", "aa CC DD EE gg PP spsp rere"),
|
|
("Algierfuchs", "AA CC DD ee GG PP spsp rere"),
|
|
("Blau", "aa CC dd EE GG PP spsp rere"),
|
|
("Gold", "AA CC DD EE GG pp spsp rere"),
|
|
("Platin", "aa CC DD EE GG pp spsp rere"),
|
|
("Goldfuchs", "AA CC DD ee GG pp spsp rere"),
|
|
("Rotfuchs", "aa CC DD ee GG pp spsp rere"),
|
|
("dd Gold", "AA CC dd EE GG pp spsp rere"),
|
|
("dd Platin", "aa CC dd EE GG pp spsp rere"),
|
|
("Altweiss (REW)", "aa CC DD EE gg pp spsp rere"),
|
|
("Apricot (Blassfuchs)", "AA CC DD ee gg pp spsp rere"),
|
|
("Blaufuchs", "aa CC DD ee gg PP spsp rere"),
|
|
("C-Separator", "aa CC DD ee gg pp spsp rere"),
|
|
("Elfenbein", "AA CC DD EE gg pp spsp rere"),
|
|
("Kohlfuchs", "aa CC DD ee GG PP spsp rere"),
|
|
("Marder", "aa cchmcchm DD EE GG PP spsp rere"),
|
|
("Siam (Marder-Hell)", "aa cchmcchm DD EE GG PP spsp rere"),
|
|
("Polarfuchs", "AA CC DD ee gg PP spsp rere"),
|
|
("Saphir", "aa CC DD EE GG pp spsp rere"),
|
|
("Orangeschimmel", "AA CC DD efef GG PP spsp rere"),
|
|
("Topas", "AA CC DD EE GG pp spsp rere"),
|
|
("Platin-Hell", "aa CC DD EE GG pp spsp rere"),
|
|
("Agouti dd", "AA CC dd EE GG PP spsp rere"),
|
|
("Silberagouti dd", "AA CC dd EE gg PP spsp rere"),
|
|
("Kohlfuchs dd", "aa CC dd ee GG PP spsp rere"),
|
|
("Anthrazit dd", "aa CC dd EE gg PP spsp rere"),
|
|
("Agouti CP-Hell", "AA cchmcchm DD EE GG PP spsp rere"),
|
|
("Blaufuchs CP", "aa cchmcchm DD ee gg PP spsp rere"),
|
|
("Silberschimmel", "AA CC DD efef gg PP spsp rere"),
|
|
("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere"),
|
|
("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere"),
|
|
("Polarfuchs-Hell CP", "AA cchmcchm DD ee gg PP spsp rere"),
|
|
("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere"),
|
|
("Blaufuchsschimmel", "aa CC DD efef gg PP spsp rere"),
|
|
("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere"),
|
|
("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere"),
|
|
("Goldfuchsschimmel", "AA CC DD efef GG pp spsp rere"),
|
|
("Gold-Hell", "AA CC DD EE GG pp spsp rere"),
|
|
("Siam (Marder-Hell) dd", "aa cchmcchm dd EE GG PP spsp rere"),
|
|
("Marder dd", "aa cchmcchm dd EE GG PP spsp rere"),
|
|
("Zobel-Hell", "aa cchmcchm DD EE gg PP spsp rere"),
|
|
("Silberagouti dd CP", "AA cchmcchm dd EE gg PP spsp rere"),
|
|
("Silberagouti-Hell dd CP", "AA cchmcchm dd EE gg PP spsp rere"),
|
|
("Agouti dd CP", "AA cchmcchm dd EE GG PP spsp rere"),
|
|
("Agouti-Hell dd CP", "AA cchmcchm dd EE GG PP spsp rere"),
|
|
("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere"),
|
|
("Rotfuchsschimmel", "aa CC DD efef GG pp spsp rere"),
|
|
("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere"),
|
|
("Kohlfuchsschimmel, hell", "aa CC DD efef GG PP spsp rere"),
|
|
("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere"),
|
|
("Zobel dd", "aa cchmcchm dd EE gg PP spsp rere"),
|
|
("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere"),
|
|
("Kohlfuchs CP", "aa cchmcchm DD ee GG PP spsp rere"),
|
|
("Algierfuchs CP", "AA cchmcchm DD ee GG PP spsp rere"),
|
|
("Silberagouti CP", "AA cchmcchm DD EE gg PP spsp rere"),
|
|
("Agouti CP", "AA cchmcchm DD EE GG PP spsp rere"),
|
|
("Algierfuchs-Hell CP", "AA cchmcchm DD ee GG PP spsp rere"),
|
|
("Kohlfuchs,hell CP", "aa cchmcchm DD ee GG PP spsp rere"),
|
|
("Polarfuchs CP", "AA cchmcchm DD ee gg PP spsp rere"),
|
|
("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere"),
|
|
("Topas dd", "AA CC dd EE GG pp spsp rere"),
|
|
("Zobel-Hell dd", "aa cchmcchm dd EE gg PP spsp rere"),
|
|
("Kohlfuchsschimmel CP", "aa cchmcchm DD efef GG PP spsp rere"),
|
|
("Blaufuchs dd", "aa CC dd ee gg pp spsp rere"),
|
|
("CP-Orangeschimmel", "AA cchmcchm DD efef GG PP spsp rere"),
|
|
};
|
|
|
|
var rows = new ColorVariety[catalog.Length];
|
|
for (int i = 0; i < catalog.Length; i++)
|
|
{
|
|
rows[i] = new ColorVariety
|
|
{
|
|
// Stable, deterministic GUIDs so the HasData seed is migration-stable.
|
|
Id = new Guid($"00000000-0000-0000-0000-{(i + 1):D12}"),
|
|
Name = catalog[i].Name,
|
|
CanonicalGenotype = catalog[i].Genotype,
|
|
SortOrder = i,
|
|
};
|
|
}
|
|
modelBuilder.Entity<ColorVariety>().HasData(rows);
|
|
}
|
|
}
|