From b83e96f5524888bbbe17d0d9402afde0414140a6 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 13 Jun 2026 13:35:30 +0200 Subject: [PATCH] feat: Rennmausakte, Zucht-Suffix, Gehege-Bilder, Toasts, Infinite-Scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detailseite (Rennmausakte): - Neugestaltung: Hero-Foto + Overlay, Schnellfakten-Pills, Karten-Sektionen, Charakter als Chips, getabte Fotos/Gesundheit/Gewicht. - Eltern (Vater/Mutter) als Links; Charakter-Karte klappt bei Status Abgabe/Verstorben/Abgegeben ein (nur Zucht/Liebhaber offen). - Gehege wird bei abgegebenen/verstorbenen Tieren ausgeblendet (Detail + Formular). Listen: - Infinite Scroll auf allen Listen (Rennmäuse, Würfe, Gehege, Verträge, Anfragen, Kontakte) via useInfiniteList/useInfiniteSentinel; stabile Sortierung mit id-Tiebreaker (keine doppelten Keys), Back-to-top-Button. - Kontakte: Rolle-Filter (Züchter/Abnehmer) als Quick-Chips + Sticky-Header. Zucht-Nachname: - Namens-Anhängsel der Zucht in den Einstellungen + je Züchter-Kontakt (Backend-Spalten + Migration); eigene Tiere zeigen „Name + Suffix". - „Eigene Zucht" ist die Standard-Herkunft neuer Tiere. Weiteres: - Gehege-Bilder: Upload/Galerie auf der Gehege-Detailseite (Backend EnclosurePhoto + Endpoints + Migration, geteilte Dateiablage). - Toast-Rückmeldungen für alle Speichern-Aktionen. - Checkboxen durch mobile-freundliche Toggle-Schalter ersetzt. Co-Authored-By: Claude Opus 4.8 (1M context) --- GerbilManagerWebAPI/ApplicationContext.cs | 5 + GerbilManagerWebAPI/Dtos/ApiDtos.cs | 4 +- GerbilManagerWebAPI/Dtos/ContractDtos.cs | 3 +- .../Endpoints/ContactEndpoints.cs | 6 +- .../Endpoints/PhotoEndpoints.cs | 59 + .../Endpoints/SettingsEndpoints.cs | 3 +- ...13085850_AddBreedingNameSuffix.Designer.cs | 1453 ++++++++++++++++ .../20260613085850_AddBreedingNameSuffix.cs | 47 + ...60613093044_AddEnclosurePhotos.Designer.cs | 1491 +++++++++++++++++ .../20260613093044_AddEnclosurePhotos.cs | 49 + .../ApplicationContextModelSnapshot.cs | 46 + GerbilManagerWebAPI/Models/BreederSettings.cs | 7 + GerbilManagerWebAPI/Models/Contact.cs | 6 + GerbilManagerWebAPI/Models/EnclosurePhoto.cs | 17 + gerbil-manager-web/e2e/charakter.spec.ts | 9 +- gerbil-manager-web/e2e/mock-api.ts | 8 + gerbil-manager-web/e2e/status-model.spec.ts | 21 +- gerbil-manager-web/e2e/tiere.spec.ts | 5 +- gerbil-manager-web/src/api/contacts.ts | 1 + gerbil-manager-web/src/api/enclosurePhotos.ts | 47 + gerbil-manager-web/src/api/settings.ts | 3 + gerbil-manager-web/src/api/types.ts | 2 + .../src/components/AppShell.tsx | 5 +- .../src/components/BreederSuffixProvider.tsx | 17 + .../src/components/EnclosurePhotosSection.tsx | 116 ++ .../src/components/GerbilProfilePhoto.tsx | 30 +- .../src/components/GroupComposer.tsx | 4 +- .../src/components/ToastProvider.tsx | 43 + .../src/components/breederSuffix.ts | 17 + gerbil-manager-web/src/components/toast.css | 55 + gerbil-manager-web/src/components/toast.ts | 19 + gerbil-manager-web/src/format/gerbilName.ts | 24 + gerbil-manager-web/src/index.css | 54 +- gerbil-manager-web/src/main.tsx | 5 +- gerbil-manager-web/src/pages/AbgabePage.tsx | 4 +- .../src/pages/BeckenDetailPage.tsx | 4 + .../src/pages/BeckenFormPage.tsx | 11 +- .../src/pages/EinstellungenPage.tsx | 10 +- .../src/pages/GerbilDetailPage.tsx | 505 +++--- .../src/pages/GerbilFormPage.tsx | 44 +- gerbil-manager-web/src/pages/GerbilsPage.tsx | Bin 14242 -> 14346 bytes .../src/pages/KontaktFormPage.tsx | 37 +- gerbil-manager-web/src/pages/KontaktePage.tsx | 138 +- .../src/pages/WuerfeListPage.tsx | 20 +- .../src/pages/WurfDetailPage.tsx | 9 +- gerbil-manager-web/src/pages/WurfFormPage.tsx | 3 + gerbil-manager-web/src/pages/rennmausakte.css | 415 +++++ gerbil-manager-web/src/strings/de.ts | 20 + 48 files changed, 4601 insertions(+), 300 deletions(-) create mode 100644 GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.Designer.cs create mode 100644 GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.cs create mode 100644 GerbilManagerWebAPI/Models/EnclosurePhoto.cs create mode 100644 gerbil-manager-web/src/api/enclosurePhotos.ts create mode 100644 gerbil-manager-web/src/components/BreederSuffixProvider.tsx create mode 100644 gerbil-manager-web/src/components/EnclosurePhotosSection.tsx create mode 100644 gerbil-manager-web/src/components/ToastProvider.tsx create mode 100644 gerbil-manager-web/src/components/breederSuffix.ts create mode 100644 gerbil-manager-web/src/components/toast.css create mode 100644 gerbil-manager-web/src/components/toast.ts create mode 100644 gerbil-manager-web/src/format/gerbilName.ts create mode 100644 gerbil-manager-web/src/pages/rennmausakte.css diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs index b6b656a..c037927 100644 --- a/GerbilManagerWebAPI/ApplicationContext.cs +++ b/GerbilManagerWebAPI/ApplicationContext.cs @@ -13,6 +13,7 @@ public class ApplicationContext : DbContext public DbSet Enclosures => Set(); public DbSet ColorVarieties => Set(); public DbSet GerbilPhotos => Set(); + public DbSet EnclosurePhotos => Set(); public DbSet HealthRecords => Set(); public DbSet WeightRecords => Set(); public DbSet SaleContracts => Set(); @@ -133,6 +134,10 @@ public class ApplicationContext : DbContext e.HasOne().WithMany() .HasForeignKey(p => p.GerbilId).OnDelete(DeleteBehavior.Cascade)); + modelBuilder.Entity(e => + e.HasOne().WithMany() + .HasForeignKey(p => p.EnclosureId).OnDelete(DeleteBehavior.Cascade)); + // FEAT-13: Abgabeverträge + Zuchtprofil. modelBuilder.Entity(e => { diff --git a/GerbilManagerWebAPI/Dtos/ApiDtos.cs b/GerbilManagerWebAPI/Dtos/ApiDtos.cs index c55349e..55eda6a 100644 --- a/GerbilManagerWebAPI/Dtos/ApiDtos.cs +++ b/GerbilManagerWebAPI/Dtos/ApiDtos.cs @@ -45,7 +45,7 @@ namespace GerbilManagerWebAPI.Dtos DateOnly? ExpectedGoHomeDate, string? Notes); - public record ContactDto(Guid Id, string Name, string? Email, string? Phone, string? Address, string? Notes, bool IsBreeder, bool IsReceiver); + public record ContactDto(Guid Id, string Name, string? Email, string? Phone, string? Address, string? Notes, bool IsBreeder, bool IsReceiver, string? NameSuffix); public record EnclosureDto(Guid Id, string Name, string? Notes); @@ -97,7 +97,7 @@ namespace GerbilManagerWebAPI.Dtos DateOnly? ExpectedGoHomeDate, string? Notes); - public record ContactInput(string Name, string? Email, string? Phone, string? Address, string? Notes, bool IsBreeder, bool IsReceiver); + public record ContactInput(string Name, string? Email, string? Phone, string? Address, string? Notes, bool IsBreeder, bool IsReceiver, string? NameSuffix); public record EnclosureInput(string Name, string? Notes); diff --git a/GerbilManagerWebAPI/Dtos/ContractDtos.cs b/GerbilManagerWebAPI/Dtos/ContractDtos.cs index 0d24787..c8998a0 100644 --- a/GerbilManagerWebAPI/Dtos/ContractDtos.cs +++ b/GerbilManagerWebAPI/Dtos/ContractDtos.cs @@ -29,5 +29,6 @@ namespace GerbilManagerWebAPI.Dtos string Phone, string Email, string Homepage, - string City); + string City, + string NameSuffix); } diff --git a/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs b/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs index aabf032..0aac1eb 100644 --- a/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/ContactEndpoints.cs @@ -24,7 +24,7 @@ namespace GerbilManagerWebAPI.Endpoints group.MapPost("/", async (ContactInput input, ApplicationContext db) => { - var c = new Contact { Id = Guid.NewGuid(), Name = input.Name, Email = input.Email, Phone = input.Phone, Address = input.Address, Notes = input.Notes, IsBreeder = input.IsBreeder, IsReceiver = input.IsReceiver }; + var c = new Contact { Id = Guid.NewGuid(), Name = input.Name, Email = input.Email, Phone = input.Phone, Address = input.Address, Notes = input.Notes, IsBreeder = input.IsBreeder, IsReceiver = input.IsReceiver, NameSuffix = input.NameSuffix }; db.Contacts.Add(c); await db.SaveChangesAsync(); return TypedResults.Created($"/contacts/{c.Id}", ToDto(c)); @@ -35,7 +35,7 @@ namespace GerbilManagerWebAPI.Endpoints var c = await db.Contacts.FirstOrDefaultAsync(x => x.Id == id); if (c is null) return TypedResults.NotFound(); c.Name = input.Name; c.Email = input.Email; c.Phone = input.Phone; c.Address = input.Address; c.Notes = input.Notes; - c.IsBreeder = input.IsBreeder; c.IsReceiver = input.IsReceiver; + c.IsBreeder = input.IsBreeder; c.IsReceiver = input.IsReceiver; c.NameSuffix = input.NameSuffix; await db.SaveChangesAsync(); return TypedResults.NoContent(); }); @@ -55,6 +55,6 @@ namespace GerbilManagerWebAPI.Endpoints return app; } - private static ContactDto ToDto(Contact c) => new(c.Id, c.Name, c.Email, c.Phone, c.Address, c.Notes, c.IsBreeder, c.IsReceiver); + private static ContactDto ToDto(Contact c) => new(c.Id, c.Name, c.Email, c.Phone, c.Address, c.Notes, c.IsBreeder, c.IsReceiver, c.NameSuffix); } } diff --git a/GerbilManagerWebAPI/Endpoints/PhotoEndpoints.cs b/GerbilManagerWebAPI/Endpoints/PhotoEndpoints.cs index d4347a4..ea4f8a4 100644 --- a/GerbilManagerWebAPI/Endpoints/PhotoEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/PhotoEndpoints.cs @@ -72,6 +72,62 @@ namespace GerbilManagerWebAPI.Endpoints return TypedResults.NoContent(); }).WithTags("Photos"); + // ── Gehege (enclosure) photos — mirror the gerbil endpoints, shared file store ── + app.MapGet("/enclosures/{id:guid}/photos", + async Task>, NotFound>> (Guid id, ApplicationContext db) => + { + if (!await db.Enclosures.AnyAsync(e => e.Id == id)) return TypedResults.NotFound(); + var photos = await db.EnclosurePhotos.AsNoTracking() + .Where(p => p.EnclosureId == id) + .OrderBy(p => p.SortOrder) + .ToListAsync(); + return TypedResults.Ok(photos.Select(ToEnclosureDto).ToList()); + }).WithTags("Photos"); + + app.MapPost("/enclosures/{id:guid}/photos", + async Task, NotFound, BadRequest>> ( + Guid id, IFormFile file, [Microsoft.AspNetCore.Mvc.FromForm] string? caption, + ApplicationContext db, IConfiguration config, IWebHostEnvironment env) => + { + if (!await db.Enclosures.AnyAsync(e => e.Id == id)) return TypedResults.NotFound(); + if (file is null || file.Length == 0) return TypedResults.BadRequest("No file uploaded."); + + var ext = Path.GetExtension(file.FileName); + var fileName = $"{Guid.NewGuid():N}{ext}"; + var root = PhotoRoot(config, env); + Directory.CreateDirectory(root); + await using (var stream = File.Create(Path.Combine(root, fileName))) + await file.CopyToAsync(stream); + + int nextSort = (await db.EnclosurePhotos.Where(p => p.EnclosureId == id) + .Select(p => (int?)p.SortOrder).MaxAsync() ?? -1) + 1; + + var photo = new EnclosurePhoto + { + Id = Guid.NewGuid(), + EnclosureId = id, + FileName = fileName, + Caption = caption, + SortOrder = nextSort, + CreatedAt = DateTimeOffset.UtcNow, + }; + db.EnclosurePhotos.Add(photo); + await db.SaveChangesAsync(); + return TypedResults.Created($"/enclosure-photos/{photo.Id}", ToEnclosureDto(photo)); + }).WithTags("Photos").DisableAntiforgery(); + + app.MapDelete("/enclosure-photos/{id:guid}", + async Task> (Guid id, ApplicationContext db, IConfiguration config, IWebHostEnvironment env) => + { + var photo = await db.EnclosurePhotos.FirstOrDefaultAsync(p => p.Id == id); + if (photo is null) return TypedResults.NotFound(); + var path = Path.Combine(PhotoRoot(config, env), photo.FileName); + if (File.Exists(path)) File.Delete(path); + db.EnclosurePhotos.Remove(photo); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + }).WithTags("Photos"); + app.MapGet("/photos/files/{fileName}", Results> (string fileName, IConfiguration config, IWebHostEnvironment env) => { @@ -100,5 +156,8 @@ namespace GerbilManagerWebAPI.Endpoints private static PhotoDto ToDto(GerbilPhoto p) => new(p.Id, p.FileName, p.Caption, p.SortOrder, $"/photos/files/{p.FileName}"); + + private static PhotoDto ToEnclosureDto(EnclosurePhoto p) => + new(p.Id, p.FileName, p.Caption, p.SortOrder, $"/photos/files/{p.FileName}"); } } diff --git a/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs b/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs index cf032a4..0526a21 100644 --- a/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs +++ b/GerbilManagerWebAPI/Endpoints/SettingsEndpoints.cs @@ -33,6 +33,7 @@ namespace GerbilManagerWebAPI.Endpoints s.Email = input.Email ?? ""; s.Homepage = input.Homepage ?? ""; s.City = input.City ?? ""; + s.NameSuffix = input.NameSuffix ?? ""; await db.SaveChangesAsync(); return TypedResults.NoContent(); }); @@ -55,6 +56,6 @@ namespace GerbilManagerWebAPI.Endpoints } internal static BreederProfileDto ToDto(BreederSettings s) => - new(s.ZuchtName, s.Name, s.Address, s.Phone, s.Email, s.Homepage, s.City); + new(s.ZuchtName, s.Name, s.Address, s.Phone, s.Email, s.Homepage, s.City, s.NameSuffix); } } diff --git a/GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.Designer.cs b/GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.Designer.cs new file mode 100644 index 0000000..f1636b4 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.Designer.cs @@ -0,0 +1,1453 @@ +// +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("20260613085850_AddBreedingNameSuffix")] + partial class AddBreedingNameSuffix + { + /// + 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 efef 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", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efef 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", + 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 efef 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 efef 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 efef 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.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.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("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("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.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.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/20260613085850_AddBreedingNameSuffix.cs b/GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.cs new file mode 100644 index 0000000..6e8a293 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260613085850_AddBreedingNameSuffix.cs @@ -0,0 +1,47 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddBreedingNameSuffix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "NameSuffix", + table: "Contacts", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "NameSuffix", + table: "BreederSettings", + type: "text", + nullable: false, + defaultValue: ""); + + migrationBuilder.UpdateData( + table: "BreederSettings", + keyColumn: "Id", + keyValue: new Guid("11111111-1111-1111-1111-000000000001"), + column: "NameSuffix", + value: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "NameSuffix", + table: "Contacts"); + + migrationBuilder.DropColumn( + name: "NameSuffix", + table: "BreederSettings"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.Designer.cs b/GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.Designer.cs new file mode 100644 index 0000000..17789a5 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.Designer.cs @@ -0,0 +1,1491 @@ +// +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("20260613093044_AddEnclosurePhotos")] + partial class AddEnclosurePhotos + { + /// + 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 efef 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", + Name = "Algierfuchsschimmel", + SortOrder = 38 + }, + new + { + Id = new Guid("00000000-0000-0000-0000-000000000036"), + CanonicalGenotype = "aa CC DD efef 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", + 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 efef 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 efef 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 efef 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.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.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("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("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.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/20260613093044_AddEnclosurePhotos.cs b/GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.cs new file mode 100644 index 0000000..2438113 --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260613093044_AddEnclosurePhotos.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddEnclosurePhotos : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "EnclosurePhotos", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + EnclosureId = table.Column(type: "uuid", nullable: false), + FileName = table.Column(type: "text", nullable: false), + Caption = table.Column(type: "text", nullable: true), + SortOrder = table.Column(type: "integer", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EnclosurePhotos", x => x.Id); + table.ForeignKey( + name: "FK_EnclosurePhotos_Enclosures_EnclosureId", + column: x => x.EnclosureId, + principalTable: "Enclosures", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_EnclosurePhotos_EnclosureId", + table: "EnclosurePhotos", + column: "EnclosureId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "EnclosurePhotos"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs index db9ee2e..504c28e 100644 --- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs +++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs @@ -164,6 +164,10 @@ namespace GerbilManagerWebAPI.Migrations .IsRequired() .HasColumnType("text"); + b.Property("NameSuffix") + .IsRequired() + .HasColumnType("text"); + b.Property("Phone") .IsRequired() .HasColumnType("text"); @@ -185,6 +189,7 @@ namespace GerbilManagerWebAPI.Migrations Email = "", Homepage = "", Name = "", + NameSuffix = "", Phone = "", ZuchtName = "" }); @@ -727,6 +732,9 @@ namespace GerbilManagerWebAPI.Migrations .HasColumnType("text") .UseCollation("de-x-icu"); + b.Property("NameSuffix") + .HasColumnType("text"); + b.Property("Notes") .HasColumnType("text"); @@ -756,6 +764,35 @@ namespace GerbilManagerWebAPI.Migrations 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.Gerbil", b => { b.Property("Id") @@ -1302,6 +1339,15 @@ namespace GerbilManagerWebAPI.Migrations .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") diff --git a/GerbilManagerWebAPI/Models/BreederSettings.cs b/GerbilManagerWebAPI/Models/BreederSettings.cs index ab56ac9..93cf7f8 100644 --- a/GerbilManagerWebAPI/Models/BreederSettings.cs +++ b/GerbilManagerWebAPI/Models/BreederSettings.cs @@ -18,6 +18,13 @@ namespace GerbilManagerWebAPI.Models public string ZuchtName { get; set; } = ""; + /// + /// Namens-Anhängsel der eigenen Zucht (wie ein „Nachname"), z. B. + /// „von den kleinen Chaoten“. Wird an Namen eigener (selbst gezüchteter) + /// Tiere angehängt: „Danako von den kleinen Chaoten“. + /// + public string NameSuffix { get; set; } = ""; + /// Vor- und Nachname inkl. Anrede, z. B. „Frau Erika Muster“. public string Name { get; set; } = ""; diff --git a/GerbilManagerWebAPI/Models/Contact.cs b/GerbilManagerWebAPI/Models/Contact.cs index 5c8bcc7..9d3c093 100644 --- a/GerbilManagerWebAPI/Models/Contact.cs +++ b/GerbilManagerWebAPI/Models/Contact.cs @@ -18,5 +18,11 @@ namespace GerbilManagerWebAPI.Models public bool IsBreeder { get; set; } public bool IsReceiver { get; set; } + + /// + /// Namens-Anhängsel dieser Zucht (wie ein „Nachname"), z. B. + /// „von den Wüstenwinden“. Für Tiere fremder Züchter pflegbar. + /// + public string? NameSuffix { get; set; } } } diff --git a/GerbilManagerWebAPI/Models/EnclosurePhoto.cs b/GerbilManagerWebAPI/Models/EnclosurePhoto.cs new file mode 100644 index 0000000..feb79ba --- /dev/null +++ b/GerbilManagerWebAPI/Models/EnclosurePhoto.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; + +namespace GerbilManagerWebAPI.Models +{ + /// A photo of an enclosure (Gehege). Mirrors : + /// the file lives under the shared photo-storage root; only the file name is in the DB. + public class EnclosurePhoto + { + [Key] + public Guid Id { get; set; } + public Guid EnclosureId { get; set; } + public required string FileName { get; set; } + public string? Caption { get; set; } + public int SortOrder { get; set; } + public DateTimeOffset CreatedAt { get; set; } + } +} diff --git a/gerbil-manager-web/e2e/charakter.spec.ts b/gerbil-manager-web/e2e/charakter.spec.ts index 65bcfc1..17e96a3 100644 --- a/gerbil-manager-web/e2e/charakter.spec.ts +++ b/gerbil-manager-web/e2e/charakter.spec.ts @@ -9,13 +9,18 @@ test('Charakterbogen: Eigenschaft umschalten, speichern und im Abgabe-Komposer w await expect(page.getByRole('heading', { name: 'Krümel' })).toBeVisible() await expect(page.getByText(de.character.sectionTitle)).toBeVisible() + // DESIGN (Rennmausakte): Charakter sind anklickbare Chips (Buttons) statt Checkboxen. // Eigenschaft 'neugierig' aktivieren und speichern. - await page.getByRole('checkbox', { name: 'neugierig' }).check() + await page.getByRole('button', { name: 'neugierig', exact: true }).click() await page.getByRole('button', { name: de.character.save }).click() - await expect(page.getByRole('checkbox', { name: 'neugierig' })).toBeChecked() + await expect(page.getByRole('button', { name: 'neugierig', exact: true })).toHaveAttribute( + 'aria-pressed', + 'true', + ) // Tier zur Abgabe stellen -> erscheint im /abgabe-Komposer mit gesetzter Eigenschaft. await page.getByRole('button', { name: de.pages.abgabe.markAction }).click() + await expect(page.getByRole('button', { name: de.pages.abgabe.markAction })).toBeHidden() await page.goto('/abgabe') // Auf Krümels Tier-Abschnitt eingrenzen — andere ForSale-Tiere haben ebenfalls // Charakterbögen, deshalb darf die Assertion nicht seitenbreit nach 'neugierig' suchen. diff --git a/gerbil-manager-web/e2e/mock-api.ts b/gerbil-manager-web/e2e/mock-api.ts index 1c90ce8..36958ae 100644 --- a/gerbil-manager-web/e2e/mock-api.ts +++ b/gerbil-manager-web/e2e/mock-api.ts @@ -139,6 +139,7 @@ export async function installMockApi(page: Page): Promise { name: 'Frau Erika Muster', address: 'Musterweg 1, 12345 Musterstadt', phone: '', email: '', homepage: '', city: 'Musterstadt', + nameSuffix: '', }) } if (method === 'PUT') return json(route, 204) @@ -253,6 +254,13 @@ export async function installMockApi(page: Page): Promise { return json(route, 405) } if (path.match(/^\/photos\/[^/]+$/) && method === 'DELETE') return json(route, 204) + // Gehege-Bilder (analog zu Tier-Fotos). + m = path.match(/^\/enclosures\/([^/]+)\/photos$/) + if (m) { + if (method === 'GET') return json(route, 200, []) + return json(route, 405) + } + if (path.match(/^\/enclosure-photos\/[^/]+$/) && method === 'DELETE') return json(route, 204) m = path.match(/^\/gerbils\/([^/]+)\/inbreeding-coefficient$/) if (m) { const isKruemel = m[1] === 'kruemel' diff --git a/gerbil-manager-web/e2e/status-model.spec.ts b/gerbil-manager-web/e2e/status-model.spec.ts index 3e9875e..43351f8 100644 --- a/gerbil-manager-web/e2e/status-model.spec.ts +++ b/gerbil-manager-web/e2e/status-model.spec.ts @@ -63,21 +63,12 @@ test('Gehege-Feld Sichtbarkeit bei verstorbenen/abgegebenen Tieren', async ({ pa await expect(page.locator('dt', { hasText: t.fields.enclosure })).toBeVisible() await expect(page.locator('dd', { hasText: 'Großbecken' })).toBeVisible() - // 2. Willi ist Deceased (Verstorben) -> Gehege ausgeblendet - await page.goto('/rennmaeuse/willi') - await expect(page.locator('dt', { hasText: t.fields.enclosure })).toBeHidden() - - // 3. Pauli ist GivenAway (Abgegeben), Abnehmer ist Familie Huber (isReceiver=true) -> Gehege eingeblendet - await page.goto('/rennmaeuse/pauli') - await expect(page.locator('dt', { hasText: t.fields.enclosure })).toBeVisible() - - // 4. Pauli Breeder ist GivenAway, Abnehmer ist Zoohandlung Meier (isBreeder=true) -> Gehege eingeblendet - await page.goto('/rennmaeuse/pauli-breeder') - await expect(page.locator('dt', { hasText: t.fields.enclosure })).toBeVisible() - - // 5. Pauli Neither ist GivenAway, Abnehmer ist Weder Noch (weder Züchter noch Abnehmer) -> Gehege ausgeblendet - await page.goto('/rennmaeuse/pauli-neither') - await expect(page.locator('dt', { hasText: t.fields.enclosure })).toBeHidden() + // 2. Verstorbene UND abgegebene Tiere sind nicht mehr im eigenen Bestand + // -> Gehege ausgeblendet (unabhängig von der Abnehmer-Rolle). + for (const slug of ['willi', 'pauli', 'pauli-breeder', 'pauli-neither']) { + await page.goto(`/rennmaeuse/${slug}`) + await expect(page.locator('dt', { hasText: t.fields.enclosure })).toBeHidden() + } }) test('Verstorbene Tiere: Abnehmer und Inserat-Button ausgeblendet', async ({ page }) => { diff --git a/gerbil-manager-web/e2e/tiere.spec.ts b/gerbil-manager-web/e2e/tiere.spec.ts index 3905bf1..3f70d8f 100644 --- a/gerbil-manager-web/e2e/tiere.spec.ts +++ b/gerbil-manager-web/e2e/tiere.spec.ts @@ -178,8 +178,9 @@ test('Detailseite: Würfe als Elternteil zeigt verlinkten Wurf (ANIMAL-LITTERS)' // Wurf K als Link sichtbar const litterLink = page.getByRole('link', { name: /Wurf K/ }) await expect(litterLink).toBeVisible() - // Vater-Badge sichtbar - await expect(page.getByText(t.detail.parentLittersRoleVater)).toBeVisible() + // Vater-Rollen-Badge im Wurf-Eintrag sichtbar (die Akte zeigt „Vater" jetzt auch + // als Stammdaten-Zeile, daher gezielt auf das Badge eingrenzen). + await expect(page.locator('.badge', { hasText: t.detail.parentLittersRoleVater })).toBeVisible() // Navigation zum Wurf funktioniert await litterLink.first().click() await expect(page.getByRole('heading', { name: 'Wurf K' })).toBeVisible() diff --git a/gerbil-manager-web/src/api/contacts.ts b/gerbil-manager-web/src/api/contacts.ts index 716af08..3972430 100644 --- a/gerbil-manager-web/src/api/contacts.ts +++ b/gerbil-manager-web/src/api/contacts.ts @@ -12,6 +12,7 @@ export interface CreateContact { notes?: string | null isBreeder?: boolean isReceiver?: boolean + nameSuffix?: string | null } /** Payload for PUT /contacts/{id}. */ diff --git a/gerbil-manager-web/src/api/enclosurePhotos.ts b/gerbil-manager-web/src/api/enclosurePhotos.ts new file mode 100644 index 0000000..04832bc --- /dev/null +++ b/gerbil-manager-web/src/api/enclosurePhotos.ts @@ -0,0 +1,47 @@ +/** Fotos für Gehege (Enclosure) — analog zu api/photos, gleiche Dateiablage. */ +import { API_BASE_URL, ApiError, api } from './client' +import { de } from '../strings/de' +import { photoSrc, type GerbilPhoto } from './photos' + +/** Identical shape to a gerbil photo (PhotoDto). */ +export type EnclosurePhoto = GerbilPhoto + +export function listEnclosurePhotos(enclosureId: string): Promise { + return api.get(`/enclosures/${enclosureId}/photos`) +} + +export async function uploadEnclosurePhoto( + enclosureId: string, + file: File, + caption: string | null, +): Promise { + const form = new FormData() + form.append('file', file) + if (caption) form.append('caption', caption) + + let response: Response + try { + response = await fetch(`${API_BASE_URL}/enclosures/${enclosureId}/photos`, { + method: 'POST', + body: form, + }) + } catch { + throw new ApiError(de.api.errors.network, null) + } + if (!response.ok) { + const message = + response.status === 404 + ? de.api.errors.notFound + : response.status >= 500 + ? de.api.errors.server + : de.api.errors.unknown + throw new ApiError(message, response.status) + } + return (await response.json()) as EnclosurePhoto +} + +export function deleteEnclosurePhoto(photoId: string): Promise { + return api.delete(`/enclosure-photos/${photoId}`) +} + +export { photoSrc } diff --git a/gerbil-manager-web/src/api/settings.ts b/gerbil-manager-web/src/api/settings.ts index 248c550..198585d 100644 --- a/gerbil-manager-web/src/api/settings.ts +++ b/gerbil-manager-web/src/api/settings.ts @@ -10,6 +10,8 @@ export interface BreederProfile { homepage: string /** Ort der Unterschriftszeile („{Ort}, den {Datum}“). */ city: string + /** Namens-Anhängsel („Nachname") der eigenen Zucht, z. B. „von den kleinen Chaoten“. */ + nameSuffix: string } export const EMPTY_BREEDER_PROFILE: BreederProfile = { @@ -20,6 +22,7 @@ export const EMPTY_BREEDER_PROFILE: BreederProfile = { email: '', homepage: '', city: '', + nameSuffix: '', } /** Pflichtangaben für einen brauchbaren Vertrag (Hinweis-Logik der UI). */ diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts index 971de40..1c0cf71 100644 --- a/gerbil-manager-web/src/api/types.ts +++ b/gerbil-manager-web/src/api/types.ts @@ -114,6 +114,8 @@ export interface Contact { notes: string | null isBreeder: boolean isReceiver: boolean + /** Namens-Anhängsel dieser Zucht (für Tiere fremder Züchter). */ + nameSuffix: string | null } export interface Litter { diff --git a/gerbil-manager-web/src/components/AppShell.tsx b/gerbil-manager-web/src/components/AppShell.tsx index 4102e3b..61cde93 100644 --- a/gerbil-manager-web/src/components/AppShell.tsx +++ b/gerbil-manager-web/src/components/AppShell.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from 'react' import { NavLink, Outlet } from 'react-router-dom' import { de } from '../strings/de' import GerbilIcon from './GerbilIcon' +import { BreederSuffixProvider } from './BreederSuffixProvider' import './appShell.css' interface NavItem { @@ -83,7 +84,9 @@ export default function AppShell() {
- + + +
) diff --git a/gerbil-manager-web/src/components/BreederSuffixProvider.tsx b/gerbil-manager-web/src/components/BreederSuffixProvider.tsx new file mode 100644 index 0000000..66f3599 --- /dev/null +++ b/gerbil-manager-web/src/components/BreederSuffixProvider.tsx @@ -0,0 +1,17 @@ +/** + * ZUCHT-SUFFIX: provides the own-Zucht name suffix (from the breeder profile) once + * to the whole app. Consumers use `useGerbilName()` from ./breederSuffix. + */ +import type { ReactNode } from 'react' +import { getBreederProfile } from '../api/settings' +import { useApi } from '../hooks/useApi' +import { OwnSuffixContext } from './breederSuffix' + +export function BreederSuffixProvider({ children }: { children: ReactNode }) { + const profile = useApi(() => getBreederProfile(), []) + return ( + + {children} + + ) +} diff --git a/gerbil-manager-web/src/components/EnclosurePhotosSection.tsx b/gerbil-manager-web/src/components/EnclosurePhotosSection.tsx new file mode 100644 index 0000000..d130c4f --- /dev/null +++ b/gerbil-manager-web/src/components/EnclosurePhotosSection.tsx @@ -0,0 +1,116 @@ +/** Bilder eines Gehges — Galerie-Raster + Upload + Löschen (wie der Tier-Fotos-Tab). */ +import { useRef, useState, type FormEvent } from 'react' +import { de } from '../strings/de' +import { + deleteEnclosurePhoto, + listEnclosurePhotos, + photoSrc, + uploadEnclosurePhoto, +} from '../api/enclosurePhotos' +import { useApi, useMutation } from '../hooks/useApi' +import { useToast } from './toast' + +export default function EnclosurePhotosSection({ enclosureId }: { enclosureId: string }) { + const tp = de.pages.tierTabs.photos + const toast = useToast() + + const photos = useApi(() => listEnclosurePhotos(enclosureId), [enclosureId]) + const fileInput = useRef(null) + const [caption, setCaption] = useState('') + const [fileError, setFileError] = useState(null) + + const upload = useMutation((file: File) => + uploadEnclosurePhoto(enclosureId, file, caption.trim() === '' ? null : caption.trim()), + ) + const removal = useMutation((photoId: string) => deleteEnclosurePhoto(photoId)) + + async function onUpload(e: FormEvent) { + e.preventDefault() + const file = fileInput.current?.files?.[0] + if (!file) { + setFileError(tp.validation.fileRequired) + return + } + setFileError(null) + const r = await upload.run(file) + if (r.ok) { + toast.success(de.common.saved) + setCaption('') + if (fileInput.current) fileInput.current.value = '' + photos.reload() + } else { + toast.error(r.error) + } + } + + async function onDelete(photoId: string) { + if (!window.confirm(tp.deleteConfirm)) return + const r = await removal.run(photoId) + if (r.ok) { + toast.success(de.common.deleted) + photos.reload() + } else { + toast.error(r.error) + } + } + + const items = photos.data ?? [] + + return ( +
+
+ + + {upload.error &&
{upload.error}
} +
+ +
+
+ + {photos.loading &&

{de.common.loading}

} + {photos.error && ( +
+ {photos.error} + +
+ )} + {removal.error &&
{removal.error}
} + + {!photos.loading && !photos.error && items.length === 0 &&

{tp.empty}

} + + {items.length > 0 && ( +
    + {[...items] + .sort((a, b) => a.sortOrder - b.sortOrder) + .map((p) => ( +
  • + {p.caption +
    + {p.caption ?? ''} + +
    +
  • + ))} +
+ )} +
+ ) +} diff --git a/gerbil-manager-web/src/components/GerbilProfilePhoto.tsx b/gerbil-manager-web/src/components/GerbilProfilePhoto.tsx index 192986c..27ef62f 100644 --- a/gerbil-manager-web/src/components/GerbilProfilePhoto.tsx +++ b/gerbil-manager-web/src/components/GerbilProfilePhoto.tsx @@ -1,20 +1,26 @@ /** - * FEAT-6: Profilfoto im Detail-Kopf — erstes Foto nach sortOrder. - * Selbstständig ladend; rendert nichts, solange es keine Fotos gibt - * (oder die Foto-Endpunkte noch nicht live sind). + * FEAT-6: Profilfoto — erstes Foto nach sortOrder. Selbstständig ladend. + * Ohne Foto rendert es `fallback` (oder nichts). */ +import type { ReactNode } from 'react' import { listGerbilPhotos, photoSrc, profilePhoto } from '../api/photos' import { useApi } from '../hooks/useApi' -export default function GerbilProfilePhoto({ gerbilId }: { gerbilId: string }) { +interface GerbilProfilePhotoProps { + gerbilId: string + /** CSS class for the (default: the small detail-header avatar). */ + className?: string + /** Rendered when the gerbil has no photo yet (e.g. a hero placeholder). */ + fallback?: ReactNode +} + +export default function GerbilProfilePhoto({ + gerbilId, + className = 'profile-photo', + fallback = null, +}: GerbilProfilePhotoProps) { const photos = useApi(() => listGerbilPhotos(gerbilId), [gerbilId]) const profile = profilePhoto(photos.data ?? []) - if (!profile) return null - return ( - {profile.caption - ) + if (!profile) return <>{fallback} + return {profile.caption } diff --git a/gerbil-manager-web/src/components/GroupComposer.tsx b/gerbil-manager-web/src/components/GroupComposer.tsx index a8a5654..17464e1 100644 --- a/gerbil-manager-web/src/components/GroupComposer.tsx +++ b/gerbil-manager-web/src/components/GroupComposer.tsx @@ -10,6 +10,7 @@ import { formatDate, genderLabel } from '../format/labels' import { traitLabels } from '../format/traits' import FarbschlagImage from './FarbschlagImage' import Charakterbogen from './Charakterbogen' +import { useGerbilName } from './breederSuffix' type SaleStatus = 'free' | 'loose' | 'reserved' @@ -32,6 +33,7 @@ function groupGender(animals: Gerbil[]): string { export default function GroupComposer({ groupNumber, animals, farbschlagOf }: GroupComposerProps) { const t = de.pages.abgabe + const gerbilName = useGerbilName() const navigate = useNavigate() const [status, setStatus] = useState('free') const [reservedName, setReservedName] = useState('') @@ -203,7 +205,7 @@ export default function GroupComposer({ groupNumber, animals, farbschlagOf }: Gr
- {a.name} + {gerbilName(a) || a.name} {farbschlagOf(a)} · {genderLabel(a.gender)} {a.dateOfBirth ? ` · ${t.listing.bornOn} ${formatDate(a.dateOfBirth)}` : ''} diff --git a/gerbil-manager-web/src/components/ToastProvider.tsx b/gerbil-manager-web/src/components/ToastProvider.tsx new file mode 100644 index 0000000..80033c0 --- /dev/null +++ b/gerbil-manager-web/src/components/ToastProvider.tsx @@ -0,0 +1,43 @@ +/** Toast notifications: provider + auto-dismissing toast stack. */ +import { useCallback, useMemo, useRef, useState, type ReactNode } from 'react' +import { ToastContext, type ToastApi, type ToastVariant } from './toast' +import './toast.css' + +interface ToastItem { + id: number + message: string + variant: ToastVariant +} + +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]) + const idRef = useRef(0) + const remove = useCallback((id: number) => setToasts((ts) => ts.filter((t) => t.id !== id)), []) + + const api = useMemo(() => { + const show = (message: string, variant: ToastVariant = 'info') => { + const id = (idRef.current += 1) + setToasts((ts) => [...ts, { id, message, variant }]) + window.setTimeout(() => remove(id), 3500) + } + return { show, success: (m) => show(m, 'success'), error: (m) => show(m, 'error') } + }, [remove]) + + return ( + + {children} +
+ {toasts.map((t) => ( + + ))} +
+
+ ) +} diff --git a/gerbil-manager-web/src/components/breederSuffix.ts b/gerbil-manager-web/src/components/breederSuffix.ts new file mode 100644 index 0000000..801e1e4 --- /dev/null +++ b/gerbil-manager-web/src/components/breederSuffix.ts @@ -0,0 +1,17 @@ +/** + * ZUCHT-SUFFIX: the own-Zucht name-suffix context + the `useGerbilName()` helper. + * Kept separate from the provider component so each module has a single concern + * (and satisfies react-refresh's "components-only" rule for .tsx files). + */ +import { createContext, useContext } from 'react' +import { gerbilDisplayName } from '../format/gerbilName' +import type { Gerbil } from '../api/types' + +export const OwnSuffixContext = createContext('') + +/** Returns a function that renders a gerbil's display name incl. the Zucht suffix. */ +export function useGerbilName() { + const ownSuffix = useContext(OwnSuffixContext) + return (g: Pick): string => + gerbilDisplayName(g, ownSuffix) +} diff --git a/gerbil-manager-web/src/components/toast.css b/gerbil-manager-web/src/components/toast.css new file mode 100644 index 0000000..ebc17cc --- /dev/null +++ b/gerbil-manager-web/src/components/toast.css @@ -0,0 +1,55 @@ +/* Toast stack: centered above the mobile action bar (normal bottom inset on desktop). */ +.toast-stack { + position: fixed; + left: 50%; + bottom: calc(5rem + env(safe-area-inset-bottom)); + transform: translateX(-50%); + z-index: 100; + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; + width: max-content; + max-width: calc(100vw - 2rem); + pointer-events: none; +} +.toast { + pointer-events: auto; + cursor: pointer; + font: inherit; + font-size: 0.9rem; + font-weight: 600; + color: #fff; + background: var(--color-text); + border: none; + border-radius: 12px; + padding: 0.7rem 1.1rem; + box-shadow: 0 6px 24px rgba(43, 33, 25, 0.28); + text-align: center; + animation: toastIn 0.22s cubic-bezier(0.22, 1, 0.36, 1); +} +@keyframes toastIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: none; + } +} +.toast--success { + background: #5e8c5a; +} +.toast--error { + background: #a6433a; +} +.toast--info { + background: var(--color-accent); +} + +@media (min-width: 768px) { + .toast-stack { + bottom: 1.5rem; + } +} diff --git a/gerbil-manager-web/src/components/toast.ts b/gerbil-manager-web/src/components/toast.ts new file mode 100644 index 0000000..612c931 --- /dev/null +++ b/gerbil-manager-web/src/components/toast.ts @@ -0,0 +1,19 @@ +/** Toast notifications: the context + `useToast()` hook (kept separate from the + * provider component so each module has a single concern / fast-refresh stays happy). */ +import { createContext, useContext } from 'react' + +export type ToastVariant = 'success' | 'error' | 'info' + +export interface ToastApi { + show: (message: string, variant?: ToastVariant) => void + success: (message: string) => void + error: (message: string) => void +} + +const NOOP: ToastApi = { show: () => {}, success: () => {}, error: () => {} } + +export const ToastContext = createContext(NOOP) + +export function useToast(): ToastApi { + return useContext(ToastContext) +} diff --git a/gerbil-manager-web/src/format/gerbilName.ts b/gerbil-manager-web/src/format/gerbilName.ts new file mode 100644 index 0000000..6e35f49 --- /dev/null +++ b/gerbil-manager-web/src/format/gerbilName.ts @@ -0,0 +1,24 @@ +/** + * ZUCHT-SUFFIX: a gerbil's display name with the own-Zucht "suffix" (like a last + * name) appended — "Danako" + "von den kleinen Chaoten" → "Danako von den kleinen + * Chaoten". Per the chosen rule, only OWN-bred animals get the suffix; animals from + * other breeders keep their plain name. + */ +import type { Gerbil } from '../api/types' + +type NameParts = Pick + +/** Own-bred = no external origin recorded (no origin contact, no freetext breeder). */ +export function isOwnBred(g: NameParts): boolean { + return !g.originContactId && !(g.originBreeder && g.originBreeder.trim()) +} + +/** + * Name + own-Zucht suffix for own-bred animals; plain name otherwise. + * Returns '' for a nameless animal (callers fall back to the "(ohne Namen)" label). + */ +export function gerbilDisplayName(g: NameParts, ownSuffix: string): string { + if (!g.name) return '' + const suffix = ownSuffix.trim() + return suffix && isOwnBred(g) ? `${g.name} ${suffix}` : g.name +} diff --git a/gerbil-manager-web/src/index.css b/gerbil-manager-web/src/index.css index 7541d08..465c4e5 100644 --- a/gerbil-manager-web/src/index.css +++ b/gerbil-manager-web/src/index.css @@ -348,13 +348,65 @@ textarea { border: 1px dashed #bbb; } -/* Checkbox-style filter (label text + box on one row), vs. the stacked .field. */ +/* Checkbox-style filter (label text + toggle on one row), vs. the stacked .field. */ .field--check { flex-direction: row; align-items: center; + justify-content: space-between; gap: 0.4rem; } +/* Toggle-switch styling for boolean checkboxes + the multi-select pickers + (mobile-friendlier than a checkbox). The element stays a real — role, keyboard and form semantics are unchanged. */ +.field--check input[type='checkbox'], +.wizard-pick input[type='checkbox'], +.namegen-usages-grid input[type='checkbox'] { + appearance: none; + -webkit-appearance: none; + flex: 0 0 auto; + position: relative; + width: 2.5rem; + height: 1.45rem; + min-height: 0; + margin: 0; + border: none; + border-radius: 999px; + background: var(--color-border); + cursor: pointer; + transition: background 0.18s ease; +} +.field--check input[type='checkbox']::after, +.wizard-pick input[type='checkbox']::after, +.namegen-usages-grid input[type='checkbox']::after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: calc(1.45rem - 4px); + height: calc(1.45rem - 4px); + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 2px rgba(61, 46, 35, 0.35); + transition: transform 0.18s ease; +} +.field--check input[type='checkbox']:checked, +.wizard-pick input[type='checkbox']:checked, +.namegen-usages-grid input[type='checkbox']:checked { + background: var(--color-accent); +} +.field--check input[type='checkbox']:checked::after, +.wizard-pick input[type='checkbox']:checked::after, +.namegen-usages-grid input[type='checkbox']:checked::after { + transform: translateX(calc(2.5rem - 1.45rem)); +} +.field--check input[type='checkbox']:focus-visible, +.wizard-pick input[type='checkbox']:focus-visible, +.namegen-usages-grid input[type='checkbox']:focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; +} + @media (min-width: 768px) { .gerbil-card { grid-template-columns: 2fr 1fr 1.5fr 1fr auto; diff --git a/gerbil-manager-web/src/main.tsx b/gerbil-manager-web/src/main.tsx index 3461d9e..a9451cc 100644 --- a/gerbil-manager-web/src/main.tsx +++ b/gerbil-manager-web/src/main.tsx @@ -2,12 +2,15 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { ToastProvider } from './components/ToastProvider' import { installUnhandledRejectionGuard } from './dev/unhandledRejectionGuard' installUnhandledRejectionGuard() createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/gerbil-manager-web/src/pages/AbgabePage.tsx b/gerbil-manager-web/src/pages/AbgabePage.tsx index 436d1aa..4a2c93f 100644 --- a/gerbil-manager-web/src/pages/AbgabePage.tsx +++ b/gerbil-manager-web/src/pages/AbgabePage.tsx @@ -7,12 +7,14 @@ import type { Gerbil } from '../api/types' import { useApi } from '../hooks/useApi' import { fromDisplayString, genotypeToFarbschlag } from '../genetics' import GroupComposer from '../components/GroupComposer' +import { useGerbilName } from '../components/breederSuffix' import './abgabe.css' const UNGROUPED = 'ungrouped' export default function AbgabePage() { const t = de.pages.abgabe + const gerbilName = useGerbilName() const forSale = useApi( () => @@ -82,7 +84,7 @@ export default function AbgabePage() {
    {animals.map((a) => (
  • - {a.name} + {gerbilName(a) || a.name}