diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs index a620842..6c3646e 100644 --- a/GerbilManagerWebAPI/ApplicationContext.cs +++ b/GerbilManagerWebAPI/ApplicationContext.cs @@ -346,25 +346,28 @@ public class ApplicationContext : DbContext ("Dilute Anthrazit", "aa CC dd EE gg PP spsp rere", 31), // --- Schimmel / Fuchsschimmel (IDs 33-37) --- ("Silberschimmel", "AA CC DD efef gg PP spsp rere", 36), - ("Polarfuchsschimmel", "AA CC DD efef gg PP spsp rere", 37), - ("Algierfuchsschimmel", "AA CC DD efef GG PP spsp rere", 38), - ("Kohlfuchsschimmel", "aa CC DD efef GG PP spsp rere", 39), - ("Blaufuchsschimmel", "aa CC DD efef gg PP spsp rere", 40), + // GEN-5 (ticket 5826e8e2): *Fuchsschimmel = HET ef/e (internal 'efe'), + // not hom ef/ef — a Schimmel-modified Fox. Pure *schimmel stay efef. + ("Polarfuchsschimmel", "AA CC DD efe gg PP spsp rere", 37), + ("Algierfuchsschimmel", "AA CC DD efe GG PP spsp rere", 38), + ("Kohlfuchsschimmel", "aa CC DD efe GG PP spsp rere", 39), + ("Blaufuchsschimmel", "aa CC DD efe gg PP spsp rere", 40), // --- Hell variants (IDs 38-48) --- ("Kohlfuchs, hell", "aa CC DD ee GG PP spsp rere", 41), ("Goldfuchs, hell", "AA CC DD ee GG pp spsp rere", 42), - ("Goldfuchsschimmel", "AA CC DD efef GG pp spsp rere", 43), + ("Goldfuchsschimmel", "AA CC DD efe GG pp spsp rere", 43), ("Gold-Hell", "AA CC DD EE GG pp spsp rere", 44), ("Blaufuchs, hell", "aa CC DD ee gg PP spsp rere", 45), - ("Rotfuchsschimmel", "aa CC DD efef GG pp spsp rere", 46), + ("Rotfuchsschimmel", "aa CC DD efe GG pp spsp rere", 46), ("Polarfuchs, hell", "AA CC DD ee gg PP spsp rere", 47), - ("Kohlfuchsschimmel, hell","aa CC DD efef GG PP spsp rere", 48), + ("Kohlfuchsschimmel, hell","aa CC DD efe GG PP spsp rere", 48), ("Rotfuchs, hell", "aa CC DD ee GG pp spsp rere", 49), ("Kohlfuchs-Hell", "aa CC DD ee GG PP spsp rere", 50), ("Algierfuchs, hell", "AA CC DD ee GG PP spsp rere", 51), // --- Dilute (dd) renamed variants (IDs 49-50) --- ("Dilute Topas", "AA CC dd EE GG pp spsp rere", 52), - ("Dilute Blaufuchs","aa CC dd ee gg pp spsp rere", 53), + // GEN-5 (ticket 3deab547): Blaufuchs is black-eyed; dilution is P-independent. + ("Dilute Blaufuchs","aa CC dd ee gg PP spsp rere", 53), // --- Marder / Siam / CP- series (IDs 51-66) --- ("Marder", "aa cchmcchm DD EE GG PP spsp rere", 54), ("Siam", "aa cchmch DD EE GG PP spsp rere", 55), diff --git a/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.Designer.cs b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.Designer.cs new file mode 100644 index 0000000..866a660 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.Designer.cs @@ -0,0 +1,1548 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + [DbContext(typeof(ApplicationContext))] + [Migration("20260623065520_Gen5FuchsschimmelHetSeedFix")] + partial class Gen5FuchsschimmelHetSeedFix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("PageId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("PageId"); + + b.ToTable("Blocks"); + + b.HasData( + new + { + Id = new Guid("51720002-0000-0000-0000-000000000001"), + Data = "{\"text\":\"Startseite\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000001"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000002"), + Data = "{\"text\":\"Über die Zucht\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000002"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000003"), + Data = "{\"text\":\"Abgabetiere\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000004"), + Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000004"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000005"), + Data = "{\"text\":\"Farben & Genetik\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000005"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000006"), + Data = "{\"text\":\"Kontakt\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000006"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000010"), + Data = "{\"mode\":\"auto\",\"intro\":\"\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000003"), + Type = "AbgabetiereList" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000007"), + Data = "{\"text\":\"Impressum\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000070"), + Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000007"), + Type = "RichText" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000008"), + Data = "{\"text\":\"Datenschutz\",\"level\":1}", + Order = 0, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "Heading" + }, + new + { + Id = new Guid("51720002-0000-0000-0000-000000000080"), + Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}", + Order = 1, + PageId = new Guid("51720001-0000-0000-0000-000000000008"), + Type = "RichText" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("City") + .IsRequired() + .HasColumnType("text"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text"); + + b.Property("Homepage") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + + b.Property("Phone") + .IsRequired() + .HasColumnType("text"); + + b.Property("ZuchtName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("BreederSettings"); + + b.HasData( + new + { + Id = new Guid("11111111-1111-1111-1111-000000000001"), + Address = "", + City = "", + Email = "", + Homepage = "", + Name = "", + NameSuffix = "", + Phone = "", + ZuchtName = "" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CanonicalGenotype") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("ColorVarieties"); + + b.HasData( + new + { + Id = new Guid("00000000-0000-0000-0000-000000000001"), + CanonicalGenotype = "AA chch DD EE GG pp spsp rere", + Name = "REW", + SortOrder = 0 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000002"), + CanonicalGenotype = "aa chch DD EE GG PP spsp rere", + Name = "Hermelin", + SortOrder = 1 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000003"), + CanonicalGenotype = "AA chch DD EE GG PP spsp rere", + Name = "Himalaya", + SortOrder = 2 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000004"), + CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere", + Name = "Zobel", + SortOrder = 3 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000005"), + CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + Name = "Rotaugenschimmel", + SortOrder = 4 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000006"), + CanonicalGenotype = "AA CC DD EE GG PP spsp rere", + Name = "Agouti", + SortOrder = 5 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000007"), + CanonicalGenotype = "aa CC DD EE GG PP spsp rere", + Name = "Schwarz", + SortOrder = 6 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000008"), + CanonicalGenotype = "AA CC DD EE gg PP spsp rere", + Name = "Silberagouti", + SortOrder = 7 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000009"), + CanonicalGenotype = "aa CC DD EE gg PP spsp rere", + Name = "Anthrazit", + SortOrder = 8 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000010"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs", + SortOrder = 9 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000011"), + CanonicalGenotype = "aa CC dd EE GG PP spsp rere", + Name = "Blau", + SortOrder = 10 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000012"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold", + SortOrder = 11 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000013"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin", + SortOrder = 12 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000014"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs", + SortOrder = 13 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000015"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs", + SortOrder = 14 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000016"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Gold", + SortOrder = 15 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000017"), + CanonicalGenotype = "aa CC dd EE GG pp spsp rere", + Name = "Dilute Platin", + SortOrder = 16 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000018"), + CanonicalGenotype = "aa CC DD EE gg pp spsp rere", + Name = "Altweiss (REW)", + SortOrder = 17 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000019"), + CanonicalGenotype = "AA CC DD ee gg pp spsp rere", + Name = "Apricot (Blassfuchs)", + SortOrder = 18 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000020"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs", + SortOrder = 19 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000021"), + CanonicalGenotype = "aa CC DD ee gg pp spsp rere", + Name = "C-Separator", + SortOrder = 20 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000022"), + CanonicalGenotype = "AA CC DD EE gg pp spsp rere", + Name = "Elfenbein", + SortOrder = 21 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000023"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs", + SortOrder = 22 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000024"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs", + SortOrder = 23 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000025"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Saphir", + SortOrder = 24 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000026"), + CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + Name = "Orangeschimmel", + SortOrder = 25 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000027"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Topas", + SortOrder = 26 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000028"), + CanonicalGenotype = "aa CC DD EE GG pp spsp rere", + Name = "Platin-Hell", + SortOrder = 27 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000029"), + CanonicalGenotype = "AA CC dd EE GG PP spsp rere", + Name = "Dilute Agouti", + SortOrder = 28 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000030"), + CanonicalGenotype = "AA CC dd EE gg PP spsp rere", + Name = "Dilute Silberagouti", + SortOrder = 29 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000031"), + CanonicalGenotype = "aa CC dd ee GG PP spsp rere", + Name = "Dilute Kohlfuchs", + SortOrder = 30 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000032"), + CanonicalGenotype = "aa CC dd EE gg PP spsp rere", + Name = "Dilute Anthrazit", + SortOrder = 31 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000033"), + CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + Name = "Silberschimmel", + SortOrder = 36 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000034"), + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", + Name = "Polarfuchsschimmel", + SortOrder = 37 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000035"), + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel", + SortOrder = 39 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000037"), + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", + Name = "Blaufuchsschimmel", + SortOrder = 40 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000038"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs, hell", + SortOrder = 41 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000039"), + CanonicalGenotype = "AA CC DD ee GG pp spsp rere", + Name = "Goldfuchs, hell", + SortOrder = 42 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000040"), + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", + Name = "Goldfuchsschimmel", + SortOrder = 43 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000041"), + CanonicalGenotype = "AA CC DD EE GG pp spsp rere", + Name = "Gold-Hell", + SortOrder = 44 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000042"), + CanonicalGenotype = "aa CC DD ee gg PP spsp rere", + Name = "Blaufuchs, hell", + SortOrder = 45 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000043"), + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", + Name = "Rotfuchsschimmel", + SortOrder = 46 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000044"), + CanonicalGenotype = "AA CC DD ee gg PP spsp rere", + Name = "Polarfuchs, hell", + SortOrder = 47 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000045"), + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", + Name = "Kohlfuchsschimmel, hell", + SortOrder = 48 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000046"), + CanonicalGenotype = "aa CC DD ee GG pp spsp rere", + Name = "Rotfuchs, hell", + SortOrder = 49 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000047"), + CanonicalGenotype = "aa CC DD ee GG PP spsp rere", + Name = "Kohlfuchs-Hell", + SortOrder = 50 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000048"), + CanonicalGenotype = "AA CC DD ee GG PP spsp rere", + Name = "Algierfuchs, hell", + SortOrder = 51 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000049"), + CanonicalGenotype = "AA CC dd EE GG pp spsp rere", + Name = "Dilute Topas", + SortOrder = 52 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000050"), + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", + Name = "Dilute Blaufuchs", + SortOrder = 53 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000051"), + CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere", + Name = "Marder", + SortOrder = 54 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000052"), + CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere", + Name = "Siam", + SortOrder = 55 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000053"), + CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere", + Name = "Zobel-Hell", + SortOrder = 56 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000054"), + CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere", + Name = "CP-Agouti", + SortOrder = 57 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000055"), + CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere", + Name = "CP-Silberagouti", + SortOrder = 59 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000056"), + CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere", + Name = "CP-Algierfuchs", + SortOrder = 61 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000057"), + CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere", + Name = "CP-Polarfuchs", + SortOrder = 63 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000058"), + CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere", + Name = "CP-Fuchs", + SortOrder = 65 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000059"), + CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere", + Name = "CP-Fuchs-Hell", + SortOrder = 66 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000060"), + CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere", + Name = "CP-Blaufuchs", + SortOrder = 67 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000061"), + CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel", + SortOrder = 68 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000062"), + CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere", + Name = "CP-Agouti-Hell", + SortOrder = 58 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000063"), + CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere", + Name = "CP-Silberagouti-Hell", + SortOrder = 60 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000064"), + CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere", + Name = "CP-Algierfuchs-Hell", + SortOrder = 62 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000065"), + CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere", + Name = "CP-Polarfuchs-Hell", + SortOrder = 64 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000066"), + CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere", + Name = "CP-Orangeschimmel-Hell", + SortOrder = 69 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000067"), + CanonicalGenotype = "AA CC dd ee GG PP spsp rere", + Name = "Dilute Algierfuchs", + SortOrder = 32 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000068"), + CanonicalGenotype = "AA CC dd ee GG pp spsp rere", + Name = "Dilute Goldfuchs", + SortOrder = 33 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000069"), + CanonicalGenotype = "aa CC dd ee GG pp spsp rere", + Name = "Dilute Rotfuchs", + SortOrder = 34 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000070"), + CanonicalGenotype = "AA CC dd ee gg PP spsp rere", + Name = "Dilute Polarfuchs", + SortOrder = 35 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("IsBreeder") + .HasColumnType("boolean"); + + b.Property("IsReceiver") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSuffix") + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("Phone") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.cs b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.cs new file mode 100644 index 0000000..b350637 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260623065520_Gen5FuchsschimmelHetSeedFix.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class Gen5FuchsschimmelHetSeedFix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000034"), + column: "CanonicalGenotype", + value: "AA CC DD efe gg PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000035"), + column: "CanonicalGenotype", + value: "AA CC DD efe GG PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000036"), + column: "CanonicalGenotype", + value: "aa CC DD efe GG PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000037"), + column: "CanonicalGenotype", + value: "aa CC DD efe gg PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000040"), + column: "CanonicalGenotype", + value: "AA CC DD efe GG pp spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000043"), + column: "CanonicalGenotype", + value: "aa CC DD efe GG pp spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000045"), + column: "CanonicalGenotype", + value: "aa CC DD efe GG PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000050"), + column: "CanonicalGenotype", + value: "aa CC dd ee gg PP spsp rere"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000034"), + column: "CanonicalGenotype", + value: "AA CC DD efef gg PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000035"), + column: "CanonicalGenotype", + value: "AA CC DD efef GG PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000036"), + column: "CanonicalGenotype", + value: "aa CC DD efef GG PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000037"), + column: "CanonicalGenotype", + value: "aa CC DD efef gg PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000040"), + column: "CanonicalGenotype", + value: "AA CC DD efef GG pp spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000043"), + column: "CanonicalGenotype", + value: "aa CC DD efef GG pp spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000045"), + column: "CanonicalGenotype", + value: "aa CC DD efef GG PP spsp rere"); + + migrationBuilder.UpdateData( + table: "ColorVarieties", + keyColumn: "Id", + keyValue: new Guid("00000000-0000-0000-0000-000000000050"), + column: "CanonicalGenotype", + value: "aa CC dd ee gg pp spsp rere"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs index 725b04b..1c778f1 100644 --- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs +++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs @@ -451,28 +451,28 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("00000000-0000-0000-0000-000000000034"), - CanonicalGenotype = "AA CC DD efef gg PP spsp rere", + CanonicalGenotype = "AA CC DD efe gg PP spsp rere", Name = "Polarfuchsschimmel", SortOrder = 37 }, new { Id = new Guid("00000000-0000-0000-0000-000000000035"), - CanonicalGenotype = "AA CC DD efef GG PP spsp rere", + CanonicalGenotype = "AA CC DD efe GG PP spsp rere", Name = "Algierfuchsschimmel", SortOrder = 38 }, new { Id = new Guid("00000000-0000-0000-0000-000000000036"), - CanonicalGenotype = "aa CC DD efef GG PP spsp rere", + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", Name = "Kohlfuchsschimmel", SortOrder = 39 }, new { Id = new Guid("00000000-0000-0000-0000-000000000037"), - CanonicalGenotype = "aa CC DD efef gg PP spsp rere", + CanonicalGenotype = "aa CC DD efe gg PP spsp rere", Name = "Blaufuchsschimmel", SortOrder = 40 }, @@ -493,7 +493,7 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("00000000-0000-0000-0000-000000000040"), - CanonicalGenotype = "AA CC DD efef GG pp spsp rere", + CanonicalGenotype = "AA CC DD efe GG pp spsp rere", Name = "Goldfuchsschimmel", SortOrder = 43 }, @@ -514,7 +514,7 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("00000000-0000-0000-0000-000000000043"), - CanonicalGenotype = "aa CC DD efef GG pp spsp rere", + CanonicalGenotype = "aa CC DD efe GG pp spsp rere", Name = "Rotfuchsschimmel", SortOrder = 46 }, @@ -528,7 +528,7 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("00000000-0000-0000-0000-000000000045"), - CanonicalGenotype = "aa CC DD efef GG PP spsp rere", + CanonicalGenotype = "aa CC DD efe GG PP spsp rere", Name = "Kohlfuchsschimmel, hell", SortOrder = 48 }, @@ -563,7 +563,7 @@ namespace GerbilManagerWebAPI.Migrations new { Id = new Guid("00000000-0000-0000-0000-000000000050"), - CanonicalGenotype = "aa CC dd ee gg pp spsp rere", + CanonicalGenotype = "aa CC dd ee gg PP spsp rere", Name = "Dilute Blaufuchs", SortOrder = 53 }, diff --git a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts index 5755a7b..3ea6d23 100644 --- a/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts +++ b/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts @@ -17,6 +17,8 @@ import { wildType, extractGenotypeFlags, displayGenotypeSafe, + resolveAllelePair, + inferUnknownsFromParents, } from '../genotype' import { combineLocus } from '../punnett' import { LOCI, type LocusKey } from '../loci' @@ -239,10 +241,11 @@ describe('Farbschlag catalog', () => { }) describe('Partially-unknown parents (wildcards)', () => { - it('handles an A? parent (phenotype agouti, genotype unknown)', () => { - // A? x aa at the A locus -> father gamete: 1/2 A, 1/4 (each of A,a) from "?" - // = effectively 3/4 A, 1/4 a ; mother always a. - // Offspring: 3/4 Aa, 1/4 aa. + it('GEN-5: an A? parent reads as AA (unknown copies the known allele)', () => { + // GEN-5 (ticket 3e643ef1, breeder rule): the unknown allele '?' is a COPY of + // the known partner 'A', so A? = AA. AA × aa → all Aa → 100% Agouti. + // (Previously '?' spread uniformly → 3/4 Agouti : 1/4 Schwarz, which invented + // a recessive 'a' gamete the parent demonstrably does not show.) const father = makeGenotype({ A: ['A', '?'], C: ['C', 'C'], @@ -257,10 +260,10 @@ describe('Partially-unknown parents (wildcards)', () => { const mother = fromDisplayString('aa CC DD EE GG PP spsp rere') const result = breed(father, mother) - const agouti = result.byFarbschlag.find((f) => f.farbschlag === 'Agouti')! - const schwarz = result.byFarbschlag.find((f) => f.farbschlag === 'Schwarz')! - expect(agouti.probability.text).toBe('3/4') - expect(schwarz.probability.text).toBe('1/4') + expect(result.offspring).toHaveLength(1) + expect(result.offspring[0].farbschlag).toBe('Agouti') + expect(result.offspring[0].probability.text).toBe('1') + expect(result.byFarbschlag.some((f) => f.farbschlag === 'Schwarz')).toBe(false) }) }) @@ -547,7 +550,10 @@ describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => { expect(name('AA CC dd EE GG PP spsp rere')).toBe('Dilute Agouti') expect(name('aa CC dd EE gg PP spsp rere')).toBe('Dilute Anthrazit') expect(name('aa CC dd ee GG PP spsp rere')).toBe('Dilute Kohlfuchs') - expect(name('aa CC dd ee gg pp spsp rere')).toBe('Dilute Blaufuchs') + // GEN-5 (ticket 3deab547): Blaufuchs is BLACK-eyed (P), and dilution is + // independent of the P-locus, so Dilute Blaufuchs is aa dd ee gg P- (was + // wrongly P:'p'). The pink-eyed variant is a different (REW-adjacent) colour. + expect(name('aa CC dd ee gg PP spsp rere')).toBe('Dilute Blaufuchs') expect(name('AA CC dd EE GG pp spsp rere')).toBe('Dilute Gold') expect(name('aa CC dd EE GG pp spsp rere')).toBe('Dilute Platin') }) @@ -572,10 +578,13 @@ describe('GEN-4: Dilute prefix, REW, no-bare-Fuchs', () => { it('Farbarten (categories) never appear as computed results', () => { // 'Fuchs', 'Fuchsschimmel', 'Schimmel' etc. are Farbarten — blocked by category guard. - // het ef/e now resolves to specific variety via locusToken ef/e -> 'ef' fix. + // GEN-5 (ticket 5826e8e2): het ef/e is the FUCHSSCHIMMEL family — it resolves to + // a *fuchsschimmel variety, NEVER a pure Schimmel (Rotaugen-/Orangeschimmel). expect(genotypeToFarbschlag(fromDisplayString('aa CC DD eef GG PP spsp rere'))).toBe('Kohlfuchsschimmel') - // Agouti ef/e: 'Orangeschimmel' wins (same token-set as Algierfuchsschimmel, listed first) - expect(genotypeToFarbschlag(fromDisplayString('AA CC DD eef GG PP spsp rere'))).toBe('Orangeschimmel') + // Agouti ef/e black-eyed → Algierfuchsschimmel (A:A,E:ef,G:G,P:P), NOT Orangeschimmel. + expect(genotypeToFarbschlag(fromDisplayString('AA CC DD eef GG PP spsp rere'))).toBe('Algierfuchsschimmel') + // hom ef/ef agouti black-eyed → the pure Orangeschimmel (Schimmel family). + expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG PP spsp rere'))).toBe('Orangeschimmel') // Unusual combo not in catalog -> Unbekannt (not 'Fuchsschimmel') expect(farbschlagFor(fromDisplayString('aa CC dd eef GG PP spsp rere')).unknown).toBe(true) // FK check: none of the 7 category names are in BASE_COLORS (no DB entries -> no FK risk) @@ -732,3 +741,118 @@ describe('GEN-3h: breeder bracket-notation display + E-locus e-before-ef order', expect(displayGenotypeSafe('')).toBe('') }) }) + +describe('GEN-5: unknown allele = copy of known (resolveAllelePair, breeder rule)', () => { + it('a single-unknown pair resolves to the homozygote of the KNOWN allele', () => { + expect(resolveAllelePair('A', ['A', '?'])).toEqual(['A', 'A']) + expect(resolveAllelePair('A', ['?', 'a'])).toEqual(['a', 'a']) + expect(resolveAllelePair('D', ['D', '?'])).toEqual(['D', 'D']) + expect(resolveAllelePair('E', ['e', '?'])).toEqual(['e', 'e']) // ee[-] = Fuchs + expect(resolveAllelePair('E', ['ef', '?'])).toEqual(['ef', 'ef']) // ef[-] = Schimmel + }) + it('a fully-unknown pair falls back to wild-type (markers stay unmarked)', () => { + expect(resolveAllelePair('A', ['?', '?'])).toEqual(['A', 'A']) + expect(resolveAllelePair('C', ['?', '?'])).toEqual(['C', 'C']) + expect(resolveAllelePair('Sp', ['?', '?'])).toEqual(['sp', 'sp']) // never implies Schecke + }) + it('a fully-known pair is returned unchanged', () => { + expect(resolveAllelePair('C', ['C', 'ch'])).toEqual(['C', 'ch']) + }) +}) + +describe('GEN-5: genetics-engine ticket reproductions (real stored genotypes)', () => { + const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s)) + + it('5826e8e2: ee[f] het (Fuchsschimmel) + pp → Goldfuchsschimmel, NOT Rotaugenschimmel', () => { + // Tier bf6f4507: Aa C- D- ee[f] G- pp Spsp — het ef/e is a Fuchsschimmel. + expect(name('Aa C- D- ee[f] G- pp Spsp')).toBe('Goldfuchsschimmel Schecke') + // and the pure hom ef/ef pp stays the pure Schimmel: + expect(name('AA CC DD efef GG pp spsp')).toBe('Rotaugenschimmel') + }) + + it('b034ddd2: ee[-] (e + unknown) → ee Fuchs → Algierfuchs, NOT Agouti', () => { + // Tier 33a7c1f9: Aa CC D- ee[-] Gg Pp spsp. Old engine read [e,?] as e/E → Agouti. + expect(name('Aa CC D- ee[-] Gg Pp spsp')).toBe('Algierfuchs') + }) + + it('3deab547 / efa2b232: aa cchm dd ee[-] gg P- → Dilute CP-Blaufuchs, NOT Zobel/blau/Unbekannt', () => { + // Tier 6864eaef. Non-agouti Fuchs colourpoint is NOT a marten (Zobel) — it + // derives a CP-fox base with the Dilute prefix. + expect(name('aa c[chm]c[chm] dd ee[-] gg Pp Spsp')).toBe('Dilute CP-Blaufuchs Schecke') + }) + + it('473dc345 / 5151ab20: Vance uw[d] (dense underwhite) parses (no crash) → Kohlfuchs', () => { + // Tier f31eb1f9: aa Cc[chm] D- ee Uwuw[d] PP spsp. uw[d] is the G locus; + // it must parse and NEVER render 'uw'. + const g = fromDisplayString('aa Cc[chm] D- ee Uwuw[d] PP spsp') + expect(g.G).toEqual(['G', 'g']) + expect(toDisplayString(g)).not.toContain('uw') + expect(genotypeToFarbschlag(g)).toBe('Kohlfuchs') + }) + + it('Fuchsschimmel family never resolves to a pure Schimmel variety', () => { + // Agouti het ef/e black-eyed → Algierfuchsschimmel; hom ef/ef → Orangeschimmel. + expect(name('AA CC DD eef GG PP spsp')).toBe('Algierfuchsschimmel') + expect(name('AA CC DD efef GG PP spsp')).toBe('Orangeschimmel') + }) +}) + +describe('GEN-5: no phantom colours in the expected-litter list (3e643ef1/c8ce27e2/3c46d0b4/1e7b66e6)', () => { + it('Mamta Mini (D-, Ee[-]) × Gold (D-, Ee): unknown D copies known D → no Dilute, no Unbekannt, no efef', () => { + // Real litter 98bfdf92. Both parents carry D- (unknown D) and an unknown E + // partner. The old uniform-spread invented dd / ef / 'Unbekannt' offspring. + const father = fromDisplayString('AA CC D- Ee[-] Gg PP spsp') // Mamta Mini + const mother = fromDisplayString('Aa CC D- Ee Gg pp spsp') // Gold + const result = breed(father, mother) + + const names = result.byFarbschlag.map((f) => f.farbschlag) + expect(names).not.toContain('Unbekannter Farbschlag') + expect(names.some((n) => n.startsWith('Dilute'))).toBe(false) + expect(result.offspring.every((o) => !o.genotype.includes('e[f]'))).toBe(true) + // Probabilities still sum to exactly 1. + const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0) + expect(sum).toBeCloseTo(1, 10) + // Only agouti vs silver-agouti can fall here (G locus segregates; everything else fixed). + expect(new Set(names)).toEqual(new Set(['Agouti', 'Silberagouti'])) + }) + + it('D- × D- never yields a dd (dilute) offspring at all', () => { + const p = fromDisplayString('AA CC D- EE GG PP spsp') + const result = breed(p, p) + expect(result.offspring.every((o) => !o.genotype.includes('dd'))).toBe(true) + }) +}) + +describe('GEN-5: parent inference fills unknown alleles (cc9ea3fe / 1a508c04)', () => { + it('Mamta Mini Ee[-] + homozygous ee father Geely → Ee', () => { + const child = fromDisplayString('AA CC DD Ee[-] Gg PP spsp') + const geely = fromDisplayString('aa CC DD ee gg PP spsp') // father: ee (hom fox) + const res = inferUnknownsFromParents(child, geely, null) + expect(res.genotype.E).toEqual(['E', 'e']) + expect(toDisplayString(res.genotype)).toBe('AA CC DD Ee Gg PP spsp') + expect(res.inferred).toEqual([{ locus: 'E', allele: 'e', from: 'father' }]) + }) + + it('falls back to the mother when only she is homozygous', () => { + const child = fromDisplayString('AA CC DD Ee[-] GG PP spsp') + const father = fromDisplayString('AA CC DD Ee GG PP spsp') // het → no force + const mother = fromDisplayString('aa CC DD ee GG PP spsp') // ee → forces e + const res = inferUnknownsFromParents(child, father, mother) + expect(res.genotype.E).toEqual(['E', 'e']) + expect(res.inferred).toEqual([{ locus: 'E', allele: 'e', from: 'mother' }]) + }) + + it('leaves the genotype untouched when no parent is homozygous at the unknown locus', () => { + const child = fromDisplayString('AA CC DD Ee[-] GG PP spsp') + const father = fromDisplayString('AA CC DD Ee GG PP spsp') + const res = inferUnknownsFromParents(child, father, null) + expect(res.inferred).toEqual([]) + expect(res.genotype.E).toEqual(child.E) + }) + + it('no-op when there is nothing unknown', () => { + const child = fromDisplayString('AA CC DD Ee GG PP spsp') + const res = inferUnknownsFromParents(child, child, child) + expect(res.inferred).toEqual([]) + }) +}) diff --git a/gerbil-manager-web/src/genetics/catalog.ts b/gerbil-manager-web/src/genetics/catalog.ts index 0aa0502..e1d74be 100644 --- a/gerbil-manager-web/src/genetics/catalog.ts +++ b/gerbil-manager-web/src/genetics/catalog.ts @@ -20,12 +20,12 @@ * meta rows dropped, 17 matched the frozen names). Genotypes normalized from * portal notation (c[chm]->cchm, c[h]->ch, e[f]->ef, '-'/'--' = unknown). */ -import { LOCI, LOCUS_ORDER, dominantAllele, type LocusKey } from './loci' +import { LOCUS_ORDER, dominantAllele, type LocusKey } from './loci' import { makeGenotype, + resolveAllelePair, toDisplayString, wildType, - WILDCARD, type AllelePair, type Genotype, } from './genotype' @@ -113,7 +113,11 @@ export const BASE_COLORS: readonly FarbschlagEntry[] = [ { name: 'Kohlfuchs-Hell', tokens: { A: 'a', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'kohlfuchs-hell-2.jpg' }, { name: 'Algierfuchs, hell', tokens: { A: 'A', C: 'C', D: 'D', E: 'e', G: 'G', P: 'P' }, image: 'algierfuchs-hell.JPG' }, { name: 'Dilute Topas', tokens: { A: 'A', C: 'C', D: 'd', E: 'E', G: 'G', P: 'p' }, image: 'topas-dd.jpg' }, - { name: 'Dilute Blaufuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'p' }, image: 'blaufuchs-dd.jpg' }, + // GEN-5 (ticket 3deab547): Blaufuchs is black-eyed (P, line 75); dilution dd is + // independent of the eye-pigment P-locus, so the dilute form is ALSO P:'P' + // (was P:'p', which made it an unreachable phantom and left dd CP-fox animals + // 'Unbekannt'/'blau'). Now aa cchm dd ee gg P- → 'Dilute CP-Blaufuchs'. + { name: 'Dilute Blaufuchs', tokens: { A: 'a', C: 'C', D: 'd', E: 'e', G: 'g', P: 'P' }, image: 'blaufuchs-dd.jpg' }, // ── GEN-3f/3g: c^chm colourpoint varieties ── // GEN-3f: aa points = marten/sable group (Marder/Siam, +gg Zobel/Zobel-Hell). @@ -163,13 +167,10 @@ export interface FarbschlagMatch { * variety specifically. */ function locusToken(g: Genotype, locus: LocusKey): string { - // Default an unknown allele to the WILD-TYPE reading: most-dominant for the - // colour loci (unknown-C => full-colour 'C', not a white), but the recessive - // UNMARKED allele for the spotting/rex markers (unknown-Sp must NOT imply Schecke). - const alleles = LOCI[locus].alleles - const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls' - const fallback = isMarker ? alleles[alleles.length - 1] : alleles[0] - const [x, y] = g[locus].map((a) => (a === WILDCARD ? fallback : a)) + // GEN-5: an unknown allele is a COPY of the known partner (resolveAllelePair), + // so e.g. [e,?] reads as ee (Fuchs), NOT e/E. Only a fully-unknown locus falls + // back to the wild-type reading (most-dominant colour / unmarked marker). + const [x, y] = resolveAllelePair(locus, g[locus]) if (locus === 'E') { if (x === y) return x // ee->'e', efef->'ef', EE->'E' // GEN-4: het ef/e → 'ef' (ef is dominant for the Schimmel phenotype; @@ -195,29 +196,50 @@ function matches(g: Genotype, entry: FarbschlagEntry): boolean { * computed farbschlag output (the farbschlagFor category guard blocks them). */ function eFamily(g: Genotype): string | null { - const [x, y] = g.E + // GEN-5: resolve unknown E as a copy of the known allele first ([e,?]→ee Fuchs, + // [ef,?]→ef/ef Schimmel, [E,?]→EE full), so families are decided consistently. + const [x, y] = resolveAllelePair('E', g.E) if (x === 'e' && y === 'e') return 'Fuchs' if ((x === 'e' && y === 'ef') || (x === 'ef' && y === 'e')) return 'Fuchsschimmel' if (x === 'ef' && y === 'ef') return 'Schimmel' - if ((x === 'e' || y === 'e') && (x === WILDCARD || y === WILDCARD)) return 'Fuchs' return null } /** Resolve a genotype to its German Farbschlag (with Schecke/Rex modifiers). */ -/** Resolve an allele pair to concrete alleles, defaulting unknown to wild-type. */ +/** + * Resolve an allele pair to concrete alleles. GEN-5: an unknown allele copies the + * known partner (resolveAllelePair); a fully-unknown locus falls back to wild-type. + */ function resolvedPair(g: Genotype, locus: LocusKey): [string, string] { - const alleles = LOCI[locus].alleles - const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls' - const fallback = isMarker ? alleles[alleles.length - 1] : alleles[0] - const [x, y] = g[locus].map((a) => (a === WILDCARD ? fallback : a)) - return [x, y] + return resolveAllelePair(locus, g[locus]) +} + +/** + * Whether a catalog entry belongs to the given E-family by NAME. The Schimmel + * entries split into two breeder groups that share the same E:'ef' token but + * differ by zygosity of the live animal: + * - 'Fuchsschimmel' family (ef/e het) → only *fuchsschimmel entries + * (Goldfuchsschimmel, Kohlfuchsschimmel, …). + * - 'Schimmel' family (ef/ef hom) → the pure roan entries whose name ends + * in 'schimmel' but NOT 'fuchsschimmel' (Rotaugenschimmel, Orangeschimmel, + * Silberschimmel, …). + * GEN-5 (ticket 5826e8e2): this is why ef/e must NOT match a pure-Schimmel entry + * (Rotaugenschimmel) — a het Fuchsschimmel animal is a Goldfuchsschimmel. + */ +function entryInEFamily(entry: FarbschlagEntry, family: string): boolean { + if (entry.tokens.E === undefined) return false + const n = entry.name.toLowerCase() + if (family === 'Fuchsschimmel') return n.includes('fuchsschimmel') + if (family === 'Schimmel') return n.includes('schimmel') && !n.includes('fuchsschimmel') + // 'Fuchs' family: fox entries are E:'e' (no 'schimmel' in the name). + return !n.includes('schimmel') } /** Base colour name (no modifiers, no colourpoint prefix), via E-family + matches. */ function baseColourFor(g: Genotype): string | null { const family = eFamily(g) const base = family - ? (BASE_COLORS.find((e) => e.tokens.E !== undefined && matches(g, e)) ?? null) + ? (BASE_COLORS.find((e) => entryInEFamily(e, family) && matches(g, e)) ?? null) : (BASE_COLORS.find((e) => matches(g, e)) ?? null) // GEN-4: never fall back to the family name — Fuchs/Fuchsschimmel/Schimmel are // Farbarten (categories), not concrete Farbschläge. If no catalog entry matches, @@ -242,13 +264,20 @@ function colourpointName(g: Genotype): string | null { // Remaining: cchm/cchm or cchm/ch (colourpoint, no full C, not chch). const bothCchm = c[0] === 'cchm' && c[1] === 'cchm' const agouti = resolvedPair(g, 'A').includes('A') - if (!agouti) { + // GEN-5 (tickets 3deab547 / efa2b232): the aa marten names (Marder/Siam/Zobel/ + // Zobel-Hell) are FULL-EXTENSION (E) sable varieties only. A non-agouti + // colourpoint that is Fuchs (ee) or Schimmel (ef) is NOT a Marder/Zobel — it + // must derive its base generically like the A- branch, so e.g. + // aa cchm dd ee gg → 'Dilute CP-Polarfuchs' (dilute + fox + grey), never Zobel. + if (!agouti && eFamily(g) === null) { const [g1, g2] = resolvedPair(g, 'G') const grey = g1 === 'g' && g2 === 'g' if (grey) return bothCchm ? 'Zobel' : 'Zobel-Hell' return bothCchm ? 'Marder' : 'Siam' } - // A- colourpoint: base as if C were full; het (cchm/ch) -> '-Hell' suffix. + // Colourpoint base derivation: name the colour as if C were full, then prefix + // 'CP-'; het (cchm/ch) gets the '-Hell' suffix. Used by A- and by non-agouti + // Fuchs/Schimmel colourpoints (which have no dedicated marten name). const base = baseColourFor(makeGenotype({ ...g, C: ['C', 'C'] })) if (!base) return null // GEN-4: if base is a Dilute variety, prefix ordering is 'Dilute CP-X' not 'CP-Dilute X'. @@ -314,12 +343,22 @@ export function genotypeToFarbschlag(g: Genotype): string { export function representativeGenotype(entry: FarbschlagEntry): Genotype { const base = wildType() const out = {} as Record + // GEN-5 (ticket 5826e8e2): a *Fuchsschimmel variety is the HETEROZYGOUS ef/e + // animal (a Schimmel-modified Fox), whereas the pure *schimmel varieties + // (Rotaugen-/Orange-/Silberschimmel) are HOMOZYGOUS ef/ef. The E token is the + // shared phenotype letter 'ef'; the representative genotype must encode the + // right zygosity so each entry round-trips back to its own family. + const isFuchsschimmel = entry.name.toLowerCase().includes('fuchsschimmel') for (const locus of LOCUS_ORDER) { const token = entry.tokens[locus] if (!token) { out[locus] = base[locus] continue } + if (locus === 'E' && token === 'ef' && isFuchsschimmel) { + out[locus] = ['ef', 'e'] // het Fuchsschimmel (ef/e), not hom ef/ef + continue + } // GEN-3f: a token may encode a HETEROZYGOUS pair as "x/y" (e.g. the het // colourpoints Siam/Zobel-Hell use C: 'cchm/ch'); otherwise it's homozygous. const [a, b] = token.includes('/') ? (token.split('/') as [string, string]) : [token, token] diff --git a/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json b/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json index 3625c49..0b1dd93 100644 --- a/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json +++ b/gerbil-manager-web/src/genetics/colorVarietySeed.backend.json @@ -236,25 +236,25 @@ }, { "name": "Polarfuchsschimmel", - "canonicalGenotype": "AA CC DD efef gg PP spsp rere", + "canonicalGenotype": "AA CC DD efe gg PP spsp rere", "sortOrder": 37, "image": "polarfuchsschimmel.jpg" }, { "name": "Algierfuchsschimmel", - "canonicalGenotype": "AA CC DD efef GG PP spsp rere", + "canonicalGenotype": "AA CC DD efe GG PP spsp rere", "sortOrder": 38, "image": "algierfuchsschimmel.jpg" }, { "name": "Kohlfuchsschimmel", - "canonicalGenotype": "aa CC DD efef GG PP spsp rere", + "canonicalGenotype": "aa CC DD efe GG PP spsp rere", "sortOrder": 39, "image": "kohlfuchsschimmel.jpg" }, { "name": "Blaufuchsschimmel", - "canonicalGenotype": "aa CC DD efef gg PP spsp rere", + "canonicalGenotype": "aa CC DD efe gg PP spsp rere", "sortOrder": 40, "image": "blaufuchsschimmel.jpg" }, @@ -272,7 +272,7 @@ }, { "name": "Goldfuchsschimmel", - "canonicalGenotype": "AA CC DD efef GG pp spsp rere", + "canonicalGenotype": "AA CC DD efe GG pp spsp rere", "sortOrder": 43, "image": "goldfuchsschimmel.jpg" }, @@ -290,7 +290,7 @@ }, { "name": "Rotfuchsschimmel", - "canonicalGenotype": "aa CC DD efef GG pp spsp rere", + "canonicalGenotype": "aa CC DD efe GG pp spsp rere", "sortOrder": 46, "image": "rotfuchsschimmel.jpg" }, @@ -302,7 +302,7 @@ }, { "name": "Kohlfuchsschimmel, hell", - "canonicalGenotype": "aa CC DD efef GG PP spsp rere", + "canonicalGenotype": "aa CC DD efe GG PP spsp rere", "sortOrder": 48, "image": "kohlfuchsschimmel-hell.jpg" }, @@ -332,7 +332,7 @@ }, { "name": "Dilute Blaufuchs", - "canonicalGenotype": "aa CC dd ee gg pp spsp rere", + "canonicalGenotype": "aa CC dd ee gg PP spsp rere", "sortOrder": 53, "image": "blaufuchs-dd.jpg" }, diff --git a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json index cf58486..f598579 100644 --- a/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json +++ b/gerbil-manager-web/src/genetics/colorVarietySeed.generated.json @@ -236,25 +236,25 @@ }, { "name": "Polarfuchsschimmel", - "canonicalGenotype": "AA CC DD e[f]e[f] gg PP spsp", + "canonicalGenotype": "AA CC DD ee[f] gg PP spsp", "sortOrder": 37, "image": "polarfuchsschimmel.jpg" }, { "name": "Algierfuchsschimmel", - "canonicalGenotype": "AA CC DD e[f]e[f] GG PP spsp", + "canonicalGenotype": "AA CC DD ee[f] GG PP spsp", "sortOrder": 38, "image": "algierfuchsschimmel.jpg" }, { "name": "Kohlfuchsschimmel", - "canonicalGenotype": "aa CC DD e[f]e[f] GG PP spsp", + "canonicalGenotype": "aa CC DD ee[f] GG PP spsp", "sortOrder": 39, "image": "kohlfuchsschimmel.jpg" }, { "name": "Blaufuchsschimmel", - "canonicalGenotype": "aa CC DD e[f]e[f] gg PP spsp", + "canonicalGenotype": "aa CC DD ee[f] gg PP spsp", "sortOrder": 40, "image": "blaufuchsschimmel.jpg" }, @@ -272,7 +272,7 @@ }, { "name": "Goldfuchsschimmel", - "canonicalGenotype": "AA CC DD e[f]e[f] GG pp spsp", + "canonicalGenotype": "AA CC DD ee[f] GG pp spsp", "sortOrder": 43, "image": "goldfuchsschimmel.jpg" }, @@ -290,7 +290,7 @@ }, { "name": "Rotfuchsschimmel", - "canonicalGenotype": "aa CC DD e[f]e[f] GG pp spsp", + "canonicalGenotype": "aa CC DD ee[f] GG pp spsp", "sortOrder": 46, "image": "rotfuchsschimmel.jpg" }, @@ -302,7 +302,7 @@ }, { "name": "Kohlfuchsschimmel, hell", - "canonicalGenotype": "aa CC DD e[f]e[f] GG PP spsp", + "canonicalGenotype": "aa CC DD ee[f] GG PP spsp", "sortOrder": 48, "image": "kohlfuchsschimmel-hell.jpg" }, @@ -332,7 +332,7 @@ }, { "name": "Dilute Blaufuchs", - "canonicalGenotype": "aa CC dd ee gg pp spsp", + "canonicalGenotype": "aa CC dd ee gg PP spsp", "sortOrder": 53, "image": "blaufuchs-dd.jpg" }, diff --git a/gerbil-manager-web/src/genetics/genotype.ts b/gerbil-manager-web/src/genetics/genotype.ts index eb98135..8a6d1f5 100644 --- a/gerbil-manager-web/src/genetics/genotype.ts +++ b/gerbil-manager-web/src/genetics/genotype.ts @@ -40,6 +40,38 @@ export function canonicalPair(locus: LocusKey, a: string, b: string): AllelePair return rank(a) <= rank(b) ? [a, b] : [b, a] } +/** + * GEN-5 — the breeder's UNKNOWN-allele rule (ticket 3e643ef1, confirmed by the + * Züchterin): an unknown allele '?' is a COPY of the known, visible partner + * allele until the gene is determined. So a single-unknown pair resolves to the + * homozygote of the KNOWN allele: + * A? → AA D? → DD [e,?] → ee [E,?] → EE [ef,?] → ef/ef + * Only when BOTH alleles are unknown is the locus genuinely undetermined; it then + * falls back to the wild-type reading (most-dominant colour allele, but the + * recessive UNMARKED allele for the Sp/Re/Sls markers so an unknown marker never + * implies Schecke/Rex/WP). + * + * This single rule is shared by phenotype/catalog resolution (catalog.ts) and the + * Punnett gamete weights (punnett.ts), so an unknown allele never invents a + * recessive phenotype (no phantom Dilute/efef/Unbekannt in offspring lists). + */ +export function resolveAllelePair(locus: LocusKey, pair: AllelePair): [string, string] { + const [a, b] = pair + const aUnknown = a === WILDCARD + const bUnknown = b === WILDCARD + if (!aUnknown && !bUnknown) return [a, b] + if (aUnknown && bUnknown) { + // Fully unknown: wild-type reading (markers default to the unmarked recessive). + const alleles = LOCI[locus].alleles + const isMarker = locus === 'Sp' || locus === 'Re' || locus === 'Sls' + const fb = isMarker ? alleles[alleles.length - 1] : alleles[0] + return [fb, fb] + } + // Exactly one unknown → copy of the known partner allele (homozygous). + const known = aUnknown ? b : a + return [known, known] +} + function assertAllele(locus: LocusKey, allele: string): void { if (allele === WILDCARD) return if (!LOCI[locus].alleles.includes(allele)) { @@ -180,6 +212,11 @@ function normalizeToken(tok: string): string | null { let t = tok if (t === 'WP') t = 'Slsl' t = t.replace(/S\(l\)/g, 'Sl').replace(/s\(l\)/g, 'sl') + // GEN-5: dense-underwhite modifier uw[d]/Uw[d] (G-locus). The German "[d]" + // dense marker is a shade qualifier, not a separate allele — strip it BEFORE + // the Uw→G alias so e.g. "Uwuw[d]" / "uw[d]uw[d]" parse as Gg / gg, not "Gg[d]" + // (which crashes splitToken). Mirrors tools/import/genotype.py _rewrite_uw. + t = t.replace(/uw\[d\]/gi, 'uw') t = t.replace(/Uw/g, 'G').replace(/uw/g, 'g') // GEN-3h: accept bracket display notation → canonical internal symbols. t = t.replace(/e\[f\]/g, 'ef') // Schimmel allele display form → internal @@ -284,3 +321,75 @@ export function displayGenotypeSafe(raw: string | null | undefined): string { export function hasUnknown(g: Genotype): boolean { return LOCUS_ORDER.some((l) => g[l][0] === WILDCARD || g[l][1] === WILDCARD) } + +/** Per-locus note about an allele that parent-inference filled in. */ +export interface ParentInferredLocus { + readonly locus: LocusKey + /** The allele a homozygous parent forced onto the child. */ + readonly allele: string + /** 'father' | 'mother' — which parent was homozygous. */ + readonly from: 'father' | 'mother' +} + +export interface ParentInferenceResult { + readonly genotype: Genotype + /** Loci whose unknown allele was resolved from a parent (empty = nothing changed). */ + readonly inferred: ParentInferredLocus[] +} + +/** + * GEN-5 (tickets cc9ea3fe / 1a508c04, breeder rule via Mendel): a child's UNKNOWN + * allele can be filled in from a HOMOZYGOUS parent, which can only pass that one + * allele. E.g. Mamta Mini stored E = [E,?]; her father Geely is ee (homozygous + * fox) so he must pass an 'e' — the child's unknown E allele therefore IS 'e', + * giving Ee (not the copy-of-known EE default). + * + * Rule, per locus, ONLY for an allele still unknown ('?') in the child: + * - if a parent is homozygous (both alleles equal and known), that allele is + * forced onto the child's unknown slot. + * - the father is checked first; if he doesn't resolve it, the mother is tried. + * - a parent allele is only accepted if it is one the child could legitimately + * carry at that locus (it always is for a real parent, but we guard anyway). + * Pairs with no unknown, or where no parent is homozygous, are left untouched + * (still subject to the copy-of-known display/colour rule elsewhere). + */ +export function inferUnknownsFromParents( + child: Genotype, + father: Genotype | null | undefined, + mother: Genotype | null | undefined, +): ParentInferenceResult { + const out = {} as Record + const inferred: ParentInferredLocus[] = [] + for (const locus of LOCUS_ORDER) { + const [a, b] = child[locus] + const aUnknown = a === WILDCARD + const bUnknown = b === WILDCARD + if (!aUnknown && !bUnknown) { + out[locus] = child[locus] + continue + } + const homForced = (p: Genotype | null | undefined): string | null => { + if (!p) return null + const [pa, pb] = p[locus] + if (pa === WILDCARD || pb === WILDCARD) return null + return pa === pb ? pa : null + } + const fatherAllele = homForced(father) + const forced = fatherAllele ?? homForced(mother) + const from: 'father' | 'mother' = fatherAllele ? 'father' : 'mother' + if (forced && (aUnknown !== bUnknown)) { + // Exactly one unknown slot → fill it with the forced parent allele. + const known = aUnknown ? b : a + out[locus] = canonicalPair(locus, known, forced) + inferred.push({ locus, allele: forced, from }) + } else if (forced && aUnknown && bUnknown) { + // Both unknown but a parent is homozygous → that allele is certain on one + // slot; the other stays unknown. + out[locus] = canonicalPair(locus, forced, WILDCARD) + inferred.push({ locus, allele: forced, from }) + } else { + out[locus] = child[locus] + } + } + return { genotype: makeGenotype(out), inferred } +} diff --git a/gerbil-manager-web/src/genetics/index.ts b/gerbil-manager-web/src/genetics/index.ts index d439aef..b5a2eef 100644 --- a/gerbil-manager-web/src/genetics/index.ts +++ b/gerbil-manager-web/src/genetics/index.ts @@ -21,9 +21,16 @@ export { fromJSON, hasUnknown, displayGenotypeSafe, + resolveAllelePair, + inferUnknownsFromParents, WILDCARD, } from './genotype' -export type { Genotype, AllelePair } from './genotype' +export type { + Genotype, + AllelePair, + ParentInferenceResult, + ParentInferredLocus, +} from './genotype' export { LOCI, LOCUS_ORDER } from './loci' export type { LocusKey, LocusDef } from './loci' diff --git a/gerbil-manager-web/src/genetics/punnett.ts b/gerbil-manager-web/src/genetics/punnett.ts index 23c583b..0995fc4 100644 --- a/gerbil-manager-web/src/genetics/punnett.ts +++ b/gerbil-manager-web/src/genetics/punnett.ts @@ -10,12 +10,12 @@ * Wildcard ("?") alleles are expanded uniformly over the locus' allele set * before combining, so a parent known only by phenotype can still be paired. */ -import { add, frac, multiply, ONE, type Fraction } from './fraction' -import { LOCI, LOCUS_ORDER, type LocusKey } from './loci' +import { add, frac, multiply, type Fraction, ONE } from './fraction' +import { LOCUS_ORDER, type LocusKey } from './loci' import { canonicalPair, + resolveAllelePair, toDisplayString, - WILDCARD, type AllelePair, type Genotype, } from './genotype' @@ -26,22 +26,24 @@ export interface DistEntry { readonly probability: Fraction } -/** Expand a (possibly wildcard) parent allele pair into weighted concrete alleles. */ +/** + * Expand a (possibly partly-unknown) parent allele pair into weighted concrete + * gamete alleles. + * + * GEN-5 (ticket 3e643ef1, breeder rule): an unknown allele '?' is a COPY of the + * known partner allele (A?→AA, D?→DD, [e,?]→ee), NOT a uniform spread over the + * whole allele set. Spreading wrongly invented recessive gametes (d, ef, e) that + * produced impossible offspring colours — phantom Dilute, efef Schimmel and + * 'Unbekannter Farbschlag' in the expected-litter list. After resolution each of + * the two (now concrete) alleles contributes 1/2 of the gamete. A fully-unknown + * locus resolves to the wild-type homozygote (see resolveAllelePair). + */ function parentAlleleWeights(locus: LocusKey, pair: AllelePair): Map { const weights = new Map() const addWeight = (allele: string, w: Fraction) => { weights.set(allele, add(weights.get(allele) ?? frac(0, 1), w)) } - const alleles = LOCI[locus].alleles - for (const a of pair) { - if (a === WILDCARD) { - // Unknown allele: uniform over the locus set, each contributing 1/2 of the gamete. - const share = frac(1, 2 * alleles.length) - for (const concrete of alleles) addWeight(concrete, share) - } else { - addWeight(a, frac(1, 2)) - } - } + for (const a of resolveAllelePair(locus, pair)) addWeight(a, frac(1, 2)) return weights } diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx index 8aa26db..c12f182 100644 --- a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx @@ -7,7 +7,14 @@ import { listColorVarieties, listContacts, listEnclosures, listLitters } from '. import { useApi, useMutation } from '../hooks/useApi' import { formatDate, genderLabel, statusLabel } from '../format/labels' import { ALL_TRAITS, TRAIT_CATEGORIES } from '../format/traits' -import { fromDisplayString, genotypeToFarbschlag, displayGenotypeSafe } from '../genetics' +import { + fromDisplayString, + genotypeToFarbschlag, + displayGenotypeSafe, + toDisplayString, + hasUnknown, + inferUnknownsFromParents, +} from '../genetics' import type { Gender, GerbilStatus } from '../api/types' import FarbschlagImage from '../components/FarbschlagImage' import GerbilHealthTab from '../components/GerbilHealthTab' @@ -138,6 +145,37 @@ export default function GerbilDetailPage() { const showEnclosure = g.status !== 'Deceased' && g.status !== 'GivenAway' const geno = describeGenotype(g.genotype) + // GEN-5 (Tickets cc9ea3fe / 1a508c04): unbekannte Gencode-Buchstaben aus einem + // reinerbigen Elternteil ergänzen (Mendel: ein reinerbiger Elternteil kann nur + // dieses eine Allel vererben). Greift nur, wenn der Genotyp ein '-' enthält UND + // mindestens ein Elternteil mit Genotyp am eigenen Wurf hinterlegt ist. + const genoInferred = (() => { + if (!g.genotype) return null + let child + try { + child = fromDisplayString(g.genotype) + } catch { + return null + } + if (!hasUnknown(child)) return null + const parse = (s: string | null | undefined) => { + if (!s) return null + try { + return fromDisplayString(s) + } catch { + return null + } + } + const f = parse(father.data?.genotype) + const m = parse(mother.data?.genotype) + if (!f && !m) return null + const res = inferUnknownsFromParents(child, f, m) + if (res.inferred.length === 0) return null + const display = toDisplayString(res.genotype) + if (display === geno?.display) return null + return { display, farbschlag: genotypeToFarbschlag(res.genotype) } + })() + const lookup = (map: Map, key: string | null) => (key ? (map.get(key) ?? '—') : '—') const storedColorName = g.colorVarietyId ? (colorName.get(g.colorVarietyId) ?? null) : null @@ -342,14 +380,29 @@ export default function GerbilDetailPage() {
{geno.display} + {genoInferred && ( + + {' → '} + {genoInferred.display}{' '} + ⮑ {t.detail.genotypeInferred} + + )} - {geno.farbschlag} - {storedColorName && - geno.farbschlag !== de.genetics.unknownFarbschlag && - storedColorName !== geno.farbschlag.replace(' Schecke', '').replace(' Rex', '') && ( - ⚠ {t.detail.farbschlagMismatch} - )} + {(() => { + const resolvedFarbschlag = (genoInferred ?? geno).farbschlag + return ( + <> + {resolvedFarbschlag} + {storedColorName && + resolvedFarbschlag !== de.genetics.unknownFarbschlag && + storedColorName !== + resolvedFarbschlag.replace(' Schecke', '').replace(' Rex', '') && ( + ⚠ {t.detail.farbschlagMismatch} + )} + + ) + })()}
) : ( diff --git a/gerbil-manager-web/src/pages/rennmausakte.css b/gerbil-manager-web/src/pages/rennmausakte.css index 7305160..c19fbd1 100644 --- a/gerbil-manager-web/src/pages/rennmausakte.css +++ b/gerbil-manager-web/src/pages/rennmausakte.css @@ -286,6 +286,20 @@ color: var(--ak-warn); font-weight: 600; } +/* GEN-5: parent-inferred genotype hint (Ee[-] → Ee aus den Eltern ergänzt). */ +.ak-inferred { + white-space: nowrap; +} +.ak-inferred-chip { + display: inline-block; + background: var(--ak-tan); + color: var(--color-text); + border-radius: 999px; + padding: 1px 9px; + font-weight: 600; + font-size: 12px; + vertical-align: middle; +} /* ---------- Character chips ---------- */ .ak-cgroup { diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts index a146723..ffd9520 100644 --- a/gerbil-manager-web/src/strings/de.ts +++ b/gerbil-manager-web/src/strings/de.ts @@ -106,6 +106,10 @@ export const de = { resolvedPrefix: 'Errechnet', farbschlagMismatch: 'Weicht vom eingetragenen Farbschlag ab.', genotypeNotSet: 'Kein Genotyp hinterlegt.', + // GEN-5: ein unbekannter Gencode-Buchstabe wurde aus einem reinerbigen + // Elternteil ergänzt (z. B. Vater ee ⇒ Kind erbt e). %s = aufgelöster Genotyp. + genotypeInferred: 'Aus den Eltern ergänzt', + genotypeInferredTitle: 'Ein unbekanntes Gen wurde aus einem reinerbigen Elternteil abgeleitet.', testMating: 'Probeverpaarung', edit: 'Bearbeiten', back: 'Zurück zur Liste', diff --git a/tools/import/test_genotype.py b/tools/import/test_genotype.py index 7d9ba44..0e6a3e9 100644 --- a/tools/import/test_genotype.py +++ b/tools/import/test_genotype.py @@ -28,6 +28,11 @@ r = g.parse("uwuw") check("uwuw -> gg", r["mapped8locus"].get("G") == ["g", "g"]) r = g.parse("uw[d]uw[d]") check("uw[d]uw[d] -> gg (dense underwhite)", r["mapped8locus"].get("G") == ["g", "g"]) +# GEN-5 (ticket 5151ab20 / Vance): the het 'Uwuw[d]' (one Underwhite, one dense +# underwhite) must parse to the G locus as Gg — never crash on the [d] modifier. +r = g.parse("aa Cc[chm] D- ee Uwuw[d] PP spsp") +check("Uwuw[d] -> Gg (Vance, het dense underwhite)", r["mapped8locus"].get("G") == ["G", "g"]) +check("Uwuw[d] full string: nothing unmapped", r["unmappedTokens"] == []) # Gg and Uwuw must produce the SAME mapped locus (so they stop being a conflict) check("Gg identical to Uwuw at G locus",