diff --git a/GerbilManager.Tests/PushEndpointTests.cs b/GerbilManager.Tests/PushEndpointTests.cs
new file mode 100644
index 0000000..b3aa678
--- /dev/null
+++ b/GerbilManager.Tests/PushEndpointTests.cs
@@ -0,0 +1,44 @@
+using System.Net;
+using System.Net.Http.Json;
+using System.Text.Json;
+
+namespace GerbilManager.Tests;
+
+///
+/// WEB-PUSH: VAPID-Public-Key abrufen + Abo speichern/entfernen.
+/// (Das tatsächliche Versenden wird nicht getestet — es geht an externe Push-Dienste.)
+///
+public class PushEndpointTests : IClassFixture
+{
+ private readonly ApiFactory _factory;
+ public PushEndpointTests(ApiFactory factory) => _factory = factory;
+
+ [Fact]
+ public async Task Vapid_public_key_is_exposed()
+ {
+ var client = _factory.CreateClient();
+ var doc = JsonDocument.Parse(await client.GetStringAsync("/push/vapid-public-key")).RootElement;
+ Assert.True(doc.GetProperty("enabled").GetBoolean());
+ Assert.False(string.IsNullOrWhiteSpace(doc.GetProperty("publicKey").GetString()));
+ }
+
+ [Fact]
+ public async Task Subscribe_is_idempotent_and_unsubscribe_works()
+ {
+ var client = _factory.CreateClient();
+ var endpoint = $"https://push.example.com/{Guid.NewGuid()}";
+ var body = new { endpoint, p256dh = "abc123", auth = "def456" };
+
+ var first = await client.PostAsJsonAsync("/push/subscribe", body);
+ Assert.Equal(HttpStatusCode.OK, first.StatusCode);
+ // Erneutes Abo mit derselben Endpoint-URL -> weiterhin OK (Upsert, kein Duplikat).
+ var second = await client.PostAsJsonAsync("/push/subscribe", body);
+ Assert.Equal(HttpStatusCode.OK, second.StatusCode);
+
+ var bad = await client.PostAsJsonAsync("/push/subscribe", new { endpoint = "", p256dh = "", auth = "" });
+ Assert.Equal(HttpStatusCode.BadRequest, bad.StatusCode);
+
+ var unsub = await client.PostAsJsonAsync("/push/unsubscribe", new { endpoint });
+ Assert.Equal(HttpStatusCode.NoContent, unsub.StatusCode);
+ }
+}
diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs
index 801bdb1..7b080a2 100644
--- a/GerbilManagerWebAPI/ApplicationContext.cs
+++ b/GerbilManagerWebAPI/ApplicationContext.cs
@@ -26,6 +26,7 @@ public class ApplicationContext : DbContext
public DbSet MailSettings => Set();
public DbSet Feedback => Set();
public DbSet FeedbackAttachments => Set();
+ public DbSet WebPushSubscriptions => Set();
public DbSet AcquisitionRecords => Set();
public DbSet SaleReservations => Set();
public DbSet WaitingListEntries => Set();
diff --git a/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs b/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs
index f17bc22..a3b7afa 100644
--- a/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs
+++ b/GerbilManagerWebAPI/Dtos/FeedbackDtos.cs
@@ -83,5 +83,11 @@ namespace GerbilManagerWebAPI.Dtos
/// Set/clear the ticket category. Empty/whitespace clears it.
string? Category = null,
/// 👍/👎 on a resolved ticket. null leaves it unchanged.
- bool? Helpful = null);
+ bool? Helpful = null,
+ ///
+ /// Wenn true, wird nach dem Update eine Push-Benachrichtigung an die Züchterin gesendet.
+ /// Nur die KI/der Betreuer setzt das (z. B. neue Rückfrage / Ticket gelöst); das Frontend
+ /// setzt es NIE — so löst die Züchterin mit eigenen Aktionen keine Selbst-Pushes aus.
+ ///
+ bool? Notify = null);
}
diff --git a/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs b/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs
index a732005..f075a93 100644
--- a/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs
+++ b/GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs
@@ -91,7 +91,7 @@ namespace GerbilManagerWebAPI.Endpoints
});
group.MapPut("/{id:guid}", async Task, NotFound, BadRequest>> (
- Guid id, FeedbackUpdate input, ApplicationContext db) =>
+ Guid id, FeedbackUpdate input, ApplicationContext db, Push.PushNotifier push) =>
{
var entity = await db.Feedback.FirstOrDefaultAsync(f => f.Id == id);
if (entity is null)
@@ -205,6 +205,28 @@ namespace GerbilManagerWebAPI.Endpoints
entity.Helpful = input.Helpful;
await db.SaveChangesAsync();
+
+ // Push an die Züchterin, wenn die KI das anfordert (notify=true). Nachricht aus dem
+ // resultierenden Status ableiten. Fehler dürfen die Antwort nicht stören.
+ if (input.Notify == true && push.Enabled)
+ {
+ var name = string.IsNullOrWhiteSpace(entity.EntityName) ? "" : $" ({entity.EntityName})";
+ var (title, body) = entity.Status switch
+ {
+ "NeedsInfo" => ("Neue Rückfrage" + name, Trim(entity.Question) ?? "Bitte schau in deine Tickets."),
+ "Resolved" => ("Ticket gelöst" + name, Trim(entity.FixNote) ?? Trim(entity.Message) ?? "Erledigt."),
+ _ => ("Neues zu deinem Ticket" + name, Trim(entity.Message) ?? ""),
+ };
+ try
+ {
+ await push.NotifyAllAsync(title, body, $"/hilfe/tickets?focus={entity.Id}");
+ }
+ catch (Exception)
+ {
+ // Push ist best-effort — niemals die API-Antwort daran scheitern lassen.
+ }
+ }
+
return TypedResults.Ok(ToDto(entity));
});
@@ -309,6 +331,14 @@ namespace GerbilManagerWebAPI.Endpoints
private static readonly JsonSerializerOptions ThreadJson = new(JsonSerializerDefaults.Web);
+ /// Für Push-Texte: leeren Wert zu null, sonst auf ~140 Zeichen kürzen.
+ private static string? Trim(string? s)
+ {
+ if (string.IsNullOrWhiteSpace(s)) return null;
+ var t = s.Trim();
+ return t.Length > 140 ? t[..139] + "…" : t;
+ }
+
///
/// Append the ticket's current (Question, Answer) exchange to the thread/history JSON
/// before it gets overwritten by a new round, then clear the current Answer. Only the
diff --git a/GerbilManagerWebAPI/Endpoints/PushEndpoints.cs b/GerbilManagerWebAPI/Endpoints/PushEndpoints.cs
new file mode 100644
index 0000000..8b2b1eb
--- /dev/null
+++ b/GerbilManagerWebAPI/Endpoints/PushEndpoints.cs
@@ -0,0 +1,74 @@
+using GerbilManagerWebAPI.Models;
+using GerbilManagerWebAPI.Push;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.EntityFrameworkCore;
+
+namespace GerbilManagerWebAPI.Endpoints
+{
+ ///
+ /// WEB-PUSH: PWA-Benachrichtigungen für die Züchterin.
+ /// GET /push/vapid-public-key -> öffentlicher VAPID-Schlüssel (für das Abonnieren im Browser).
+ /// POST /push/subscribe -> Browser-Abo speichern (idempotent über die Endpoint-URL).
+ /// POST /push/unsubscribe -> Abo entfernen.
+ /// Das eigentliche Senden passiert über (z. B. wenn die KI eine
+ /// Rückfrage stellt oder ein Ticket löst — gesteuert über das notify-Flag auf PUT /feedback).
+ ///
+ public static class PushEndpoints
+ {
+ public record PushSubscriptionInput(string Endpoint, string P256dh, string Auth);
+ public record PushUnsubscribeInput(string Endpoint);
+
+ public static IEndpointRouteBuilder MapPushEndpoints(this IEndpointRouteBuilder app)
+ {
+ var group = app.MapGroup("/push").WithTags("Push");
+
+ group.MapGet("/vapid-public-key", (PushNotifier push) =>
+ TypedResults.Ok(new { publicKey = push.PublicKey, enabled = push.Enabled }));
+
+ group.MapPost("/subscribe", async Task>> (
+ PushSubscriptionInput input, ApplicationContext db) =>
+ {
+ if (string.IsNullOrWhiteSpace(input.Endpoint)
+ || string.IsNullOrWhiteSpace(input.P256dh)
+ || string.IsNullOrWhiteSpace(input.Auth))
+ return TypedResults.BadRequest("Endpoint, P256dh und Auth sind erforderlich.");
+
+ var existing = await db.WebPushSubscriptions
+ .FirstOrDefaultAsync(s => s.Endpoint == input.Endpoint);
+ if (existing is null)
+ {
+ db.WebPushSubscriptions.Add(new WebPushSubscription
+ {
+ Id = Guid.NewGuid(),
+ Endpoint = input.Endpoint,
+ P256dh = input.P256dh,
+ Auth = input.Auth,
+ CreatedAt = DateTimeOffset.UtcNow,
+ });
+ }
+ else
+ {
+ existing.P256dh = input.P256dh;
+ existing.Auth = input.Auth;
+ }
+ await db.SaveChangesAsync();
+ return TypedResults.Ok();
+ });
+
+ group.MapPost("/unsubscribe", async (PushUnsubscribeInput input, ApplicationContext db) =>
+ {
+ var subs = await db.WebPushSubscriptions
+ .Where(s => s.Endpoint == input.Endpoint)
+ .ToListAsync();
+ if (subs.Count > 0)
+ {
+ db.WebPushSubscriptions.RemoveRange(subs);
+ await db.SaveChangesAsync();
+ }
+ return TypedResults.NoContent();
+ });
+
+ return app;
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/GerbilManagerWebAPI.csproj b/GerbilManagerWebAPI/GerbilManagerWebAPI.csproj
index d1f12a0..03de1b1 100644
--- a/GerbilManagerWebAPI/GerbilManagerWebAPI.csproj
+++ b/GerbilManagerWebAPI/GerbilManagerWebAPI.csproj
@@ -24,6 +24,7 @@
+
diff --git a/GerbilManagerWebAPI/Migrations/20260623093055_WebPushSubscriptions.Designer.cs b/GerbilManagerWebAPI/Migrations/20260623093055_WebPushSubscriptions.Designer.cs
new file mode 100644
index 0000000..fd92812
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260623093055_WebPushSubscriptions.Designer.cs
@@ -0,0 +1,1858 @@
+//
+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("20260623093055_WebPushSubscriptions")]
+ partial class WebPushSubscriptions
+ {
+ ///
+ 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.AcquisitionRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Note")
+ .HasColumnType("text");
+
+ b.Property("Price")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.Property("SourceContactId")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("AcquisitionRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Data")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Order")
+ .HasColumnType("integer");
+
+ b.Property("PageId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PageId");
+
+ b.ToTable("Blocks");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000001"),
+ Data = "{\"text\":\"Startseite\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000001"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000002"),
+ Data = "{\"text\":\"Über die Zucht\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000002"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000003"),
+ Data = "{\"text\":\"Abgabetiere\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000003"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000004"),
+ Data = "{\"text\":\"Abgabebedingungen\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000004"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000005"),
+ Data = "{\"text\":\"Farben & Genetik\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000005"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000006"),
+ Data = "{\"text\":\"Kontakt\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000006"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000010"),
+ Data = "{\"mode\":\"auto\",\"intro\":\"\"}",
+ Order = 1,
+ PageId = new Guid("51720001-0000-0000-0000-000000000003"),
+ Type = "AbgabetiereList"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000007"),
+ Data = "{\"text\":\"Impressum\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000007"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000070"),
+ Data = "{\"markdown\":\"**Angaben gemäß § 5 TMG**\\n\\nSeitenbetreiber: [Name und vollständige Adresse eintragen]\\n\\nE-Mail: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}",
+ Order = 1,
+ PageId = new Guid("51720001-0000-0000-0000-000000000007"),
+ Type = "RichText"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000008"),
+ Data = "{\"text\":\"Datenschutz\",\"level\":1}",
+ Order = 0,
+ PageId = new Guid("51720001-0000-0000-0000-000000000008"),
+ Type = "Heading"
+ },
+ new
+ {
+ Id = new Guid("51720002-0000-0000-0000-000000000080"),
+ Data = "{\"markdown\":\"**Datenschutzerklärung**\\n\\nDiese Webseite dient der Vorstellung unserer Rennmauszucht. Es werden keine personenbezogenen Daten gespeichert oder weitergegeben.\\n\\nBei datenschutzbezogenen Fragen: [E-Mail-Adresse eintragen]\\n\\n---\\n\\n*Diese Seite wird vom Seitenbetreiber noch vervollständigt.*\"}",
+ Order = 1,
+ PageId = new Guid("51720001-0000-0000-0000-000000000008"),
+ Type = "RichText"
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.BreederSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("City")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Homepage")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("NameSuffix")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ZuchtName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("BreederSettings");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("11111111-1111-1111-1111-000000000001"),
+ Address = "",
+ City = "",
+ Email = "",
+ Homepage = "",
+ Name = "",
+ NameSuffix = "",
+ Phone = "",
+ ZuchtName = ""
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.ColorVariety", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CanonicalGenotype")
+ .HasColumnType("text");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("ColorVarieties");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000001"),
+ CanonicalGenotype = "AA chch DD EE GG pp spsp rere",
+ Name = "REW",
+ SortOrder = 0
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000002"),
+ CanonicalGenotype = "aa chch DD EE GG PP spsp rere",
+ Name = "Hermelin",
+ SortOrder = 1
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000003"),
+ CanonicalGenotype = "AA chch DD EE GG PP spsp rere",
+ Name = "Himalaya",
+ SortOrder = 2
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000004"),
+ CanonicalGenotype = "aa cchmcchm DD EE gg PP spsp rere",
+ Name = "Zobel",
+ SortOrder = 3
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000005"),
+ CanonicalGenotype = "AA CC DD efef GG pp spsp rere",
+ Name = "Rotaugenschimmel",
+ SortOrder = 4
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000006"),
+ CanonicalGenotype = "AA CC DD EE GG PP spsp rere",
+ Name = "Agouti",
+ SortOrder = 5
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000007"),
+ CanonicalGenotype = "aa CC DD EE GG PP spsp rere",
+ Name = "Schwarz",
+ SortOrder = 6
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000008"),
+ CanonicalGenotype = "AA CC DD EE gg PP spsp rere",
+ Name = "Silberagouti",
+ SortOrder = 7
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000009"),
+ CanonicalGenotype = "aa CC DD EE gg PP spsp rere",
+ Name = "Anthrazit",
+ SortOrder = 8
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000010"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs",
+ SortOrder = 9
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000011"),
+ CanonicalGenotype = "aa CC dd EE GG PP spsp rere",
+ Name = "Blau",
+ SortOrder = 10
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000012"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold",
+ SortOrder = 11
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000013"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin",
+ SortOrder = 12
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000014"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs",
+ SortOrder = 13
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000015"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs",
+ SortOrder = 14
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000016"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "Dilute Gold",
+ SortOrder = 15
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000017"),
+ CanonicalGenotype = "aa CC dd EE GG pp spsp rere",
+ Name = "Dilute Platin",
+ SortOrder = 16
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000018"),
+ CanonicalGenotype = "aa CC DD EE gg pp spsp rere",
+ Name = "Altweiss (REW)",
+ SortOrder = 17
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000019"),
+ CanonicalGenotype = "AA CC DD ee gg pp spsp rere",
+ Name = "Apricot (Blassfuchs)",
+ SortOrder = 18
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000020"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs",
+ SortOrder = 19
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000021"),
+ CanonicalGenotype = "aa CC DD ee gg pp spsp rere",
+ Name = "C-Separator",
+ SortOrder = 20
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000022"),
+ CanonicalGenotype = "AA CC DD EE gg pp spsp rere",
+ Name = "Elfenbein",
+ SortOrder = 21
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000023"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs",
+ SortOrder = 22
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000024"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs",
+ SortOrder = 23
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000025"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Saphir",
+ SortOrder = 24
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000026"),
+ CanonicalGenotype = "AA CC DD efef GG PP spsp rere",
+ Name = "Orangeschimmel",
+ SortOrder = 25
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000027"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Topas",
+ SortOrder = 26
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000028"),
+ CanonicalGenotype = "aa CC DD EE GG pp spsp rere",
+ Name = "Platin-Hell",
+ SortOrder = 27
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000029"),
+ CanonicalGenotype = "AA CC dd EE GG PP spsp rere",
+ Name = "Dilute Agouti",
+ SortOrder = 28
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000030"),
+ CanonicalGenotype = "AA CC dd EE gg PP spsp rere",
+ Name = "Dilute Silberagouti",
+ SortOrder = 29
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000031"),
+ CanonicalGenotype = "aa CC dd ee GG PP spsp rere",
+ Name = "Dilute Kohlfuchs",
+ SortOrder = 30
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000032"),
+ CanonicalGenotype = "aa CC dd EE gg PP spsp rere",
+ Name = "Dilute Anthrazit",
+ SortOrder = 31
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000033"),
+ CanonicalGenotype = "AA CC DD efef gg PP spsp rere",
+ Name = "Silberschimmel",
+ SortOrder = 36
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000034"),
+ CanonicalGenotype = "AA CC DD efe gg PP spsp rere",
+ Name = "Polarfuchsschimmel",
+ SortOrder = 37
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000035"),
+ CanonicalGenotype = "AA CC DD efe GG PP spsp rere",
+ Name = "Algierfuchsschimmel",
+ SortOrder = 38
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000036"),
+ CanonicalGenotype = "aa CC DD efe GG PP spsp rere",
+ Name = "Kohlfuchsschimmel",
+ SortOrder = 39
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000037"),
+ CanonicalGenotype = "aa CC DD efe gg PP spsp rere",
+ Name = "Blaufuchsschimmel",
+ SortOrder = 40
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000038"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs, hell",
+ SortOrder = 41
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000039"),
+ CanonicalGenotype = "AA CC DD ee GG pp spsp rere",
+ Name = "Goldfuchs, hell",
+ SortOrder = 42
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000040"),
+ CanonicalGenotype = "AA CC DD efe GG pp spsp rere",
+ Name = "Goldfuchsschimmel",
+ SortOrder = 43
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000041"),
+ CanonicalGenotype = "AA CC DD EE GG pp spsp rere",
+ Name = "Gold-Hell",
+ SortOrder = 44
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000042"),
+ CanonicalGenotype = "aa CC DD ee gg PP spsp rere",
+ Name = "Blaufuchs, hell",
+ SortOrder = 45
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000043"),
+ CanonicalGenotype = "aa CC DD efe GG pp spsp rere",
+ Name = "Rotfuchsschimmel",
+ SortOrder = 46
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000044"),
+ CanonicalGenotype = "AA CC DD ee gg PP spsp rere",
+ Name = "Polarfuchs, hell",
+ SortOrder = 47
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000045"),
+ CanonicalGenotype = "aa CC DD efe GG PP spsp rere",
+ Name = "Kohlfuchsschimmel, hell",
+ SortOrder = 48
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000046"),
+ CanonicalGenotype = "aa CC DD ee GG pp spsp rere",
+ Name = "Rotfuchs, hell",
+ SortOrder = 49
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000047"),
+ CanonicalGenotype = "aa CC DD ee GG PP spsp rere",
+ Name = "Kohlfuchs-Hell",
+ SortOrder = 50
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000048"),
+ CanonicalGenotype = "AA CC DD ee GG PP spsp rere",
+ Name = "Algierfuchs, hell",
+ SortOrder = 51
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000049"),
+ CanonicalGenotype = "AA CC dd EE GG pp spsp rere",
+ Name = "Dilute Topas",
+ SortOrder = 52
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000050"),
+ CanonicalGenotype = "aa CC dd ee gg PP spsp rere",
+ Name = "Dilute Blaufuchs",
+ SortOrder = 53
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000051"),
+ CanonicalGenotype = "aa cchmcchm DD EE GG PP spsp rere",
+ Name = "Marder",
+ SortOrder = 54
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000052"),
+ CanonicalGenotype = "aa cchmch DD EE GG PP spsp rere",
+ Name = "Siam",
+ SortOrder = 55
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000053"),
+ CanonicalGenotype = "aa cchmch DD EE gg PP spsp rere",
+ Name = "Zobel-Hell",
+ SortOrder = 56
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000054"),
+ CanonicalGenotype = "AA cchmcchm DD EE GG PP spsp rere",
+ Name = "CP-Agouti",
+ SortOrder = 57
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000055"),
+ CanonicalGenotype = "AA cchmcchm DD EE gg PP spsp rere",
+ Name = "CP-Silberagouti",
+ SortOrder = 59
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000056"),
+ CanonicalGenotype = "AA cchmcchm DD ee GG PP spsp rere",
+ Name = "CP-Algierfuchs",
+ SortOrder = 61
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000057"),
+ CanonicalGenotype = "AA cchmcchm DD ee gg PP spsp rere",
+ Name = "CP-Polarfuchs",
+ SortOrder = 63
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000058"),
+ CanonicalGenotype = "AA cchmcchm dd ee GG PP spsp rere",
+ Name = "CP-Fuchs",
+ SortOrder = 65
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000059"),
+ CanonicalGenotype = "AA cchmch dd ee GG PP spsp rere",
+ Name = "CP-Fuchs-Hell",
+ SortOrder = 66
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000060"),
+ CanonicalGenotype = "AA cchmcchm dd ee gg PP spsp rere",
+ Name = "CP-Blaufuchs",
+ SortOrder = 67
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000061"),
+ CanonicalGenotype = "AA cchmcchm DD efef GG PP spsp rere",
+ Name = "CP-Orangeschimmel",
+ SortOrder = 68
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000062"),
+ CanonicalGenotype = "AA cchmch DD EE GG PP spsp rere",
+ Name = "CP-Agouti-Hell",
+ SortOrder = 58
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000063"),
+ CanonicalGenotype = "AA cchmch DD EE gg PP spsp rere",
+ Name = "CP-Silberagouti-Hell",
+ SortOrder = 60
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000064"),
+ CanonicalGenotype = "AA cchmch DD ee GG PP spsp rere",
+ Name = "CP-Algierfuchs-Hell",
+ SortOrder = 62
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000065"),
+ CanonicalGenotype = "AA cchmch DD ee gg PP spsp rere",
+ Name = "CP-Polarfuchs-Hell",
+ SortOrder = 64
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000066"),
+ CanonicalGenotype = "AA cchmch DD efef GG PP spsp rere",
+ Name = "CP-Orangeschimmel-Hell",
+ SortOrder = 69
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000067"),
+ CanonicalGenotype = "AA CC dd ee GG PP spsp rere",
+ Name = "Dilute Algierfuchs",
+ SortOrder = 32
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000068"),
+ CanonicalGenotype = "AA CC dd ee GG pp spsp rere",
+ Name = "Dilute Goldfuchs",
+ SortOrder = 33
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000069"),
+ CanonicalGenotype = "aa CC dd ee GG pp spsp rere",
+ Name = "Dilute Rotfuchs",
+ SortOrder = 34
+ },
+ new
+ {
+ Id = new Guid("00000000-0000-0000-0000-000000000070"),
+ CanonicalGenotype = "AA CC dd ee gg PP spsp rere",
+ Name = "Dilute Polarfuchs",
+ SortOrder = 35
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Contact", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Address")
+ .HasColumnType("text");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("IsBreeder")
+ .HasColumnType("boolean");
+
+ b.Property("IsReceiver")
+ .HasColumnType("boolean");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("NameSuffix")
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("Phone")
+ .HasColumnType("text");
+
+ b.Property("Provenance")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Contacts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Capacity")
+ .HasColumnType("integer");
+
+ b.Property("CleaningCycleDays")
+ .HasColumnType("integer");
+
+ b.Property("LastCleanedDate")
+ .HasColumnType("date");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("Size")
+ .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.ExhibitionResult", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Award")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EntityName")
+ .HasColumnType("text");
+
+ b.Property("EventName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Note")
+ .HasColumnType("text");
+
+ b.Property("Placement")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("ExhibitionResults");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AgentContext")
+ .HasColumnType("text");
+
+ b.Property("Answer")
+ .HasColumnType("text");
+
+ b.Property("AnsweredAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Category")
+ .HasColumnType("text");
+
+ b.Property("ClientTimestamp")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Context")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("EntityName")
+ .HasColumnType("text");
+
+ b.Property("FixNote")
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Helpful")
+ .HasColumnType("boolean");
+
+ b.Property("LitterId")
+ .HasColumnType("uuid");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Question")
+ .HasColumnType("text");
+
+ b.Property("ReopenedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ResolvedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Thread")
+ .HasColumnType("text");
+
+ b.Property("Url")
+ .HasColumnType("text");
+
+ b.Property("UserAgent")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.ToTable("Feedback");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.FeedbackAttachment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Data")
+ .IsRequired()
+ .HasColumnType("bytea");
+
+ b.Property("FeedbackId")
+ .HasColumnType("uuid");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Size")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("FeedbackAttachments");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CauseOfDeath")
+ .HasColumnType("text");
+
+ b.Property("CharacterNote")
+ .HasColumnType("text");
+
+ b.Property("CharacterTraits")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ColorVarietyId")
+ .HasColumnType("uuid");
+
+ b.Property("DateOfBirth")
+ .HasColumnType("date");
+
+ b.Property("DateOfDeath")
+ .HasColumnType("date");
+
+ b.Property("EnclosureId")
+ .HasColumnType("uuid");
+
+ b.Property("ExternalRef")
+ .HasColumnType("text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Genotype")
+ .HasColumnType("text");
+
+ b.Property("GoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("ImportSource")
+ .HasColumnType("text");
+
+ b.Property("IsCastrated")
+ .HasColumnType("boolean");
+
+ b.Property("IsDeaf")
+ .HasColumnType("boolean");
+
+ b.Property("IsResident")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true);
+
+ b.Property("LitterId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("NameSearch")
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("OriginBreeder")
+ .HasColumnType("text")
+ .UseCollation("de-x-icu");
+
+ b.Property("OriginContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Provenance")
+ .HasColumnType("text");
+
+ b.Property("RawImportData")
+ .HasColumnType("text");
+
+ b.Property("ReceiverContactId")
+ .HasColumnType("uuid");
+
+ b.Property("SpottingType")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ColorVarietyId");
+
+ b.HasIndex("EnclosureId");
+
+ b.HasIndex("ExternalRef")
+ .IsUnique()
+ .HasFilter("\"ExternalRef\" IS NOT NULL");
+
+ b.HasIndex("LitterId");
+
+ b.HasIndex("OriginContactId");
+
+ b.HasIndex("ReceiverContactId");
+
+ b.ToTable("Gerbils");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Caption")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("SortOrder")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("GerbilPhotos");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Veterinarian")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("HealthRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Date")
+ .HasColumnType("date");
+
+ b.Property("DeathsWithin8Weeks")
+ .HasColumnType("integer");
+
+ b.Property("ExpectedGoHomeDate")
+ .HasColumnType("date");
+
+ b.Property("ExternalRef")
+ .HasColumnType("text");
+
+ b.Property("FatherId")
+ .HasColumnType("uuid");
+
+ b.Property("LitterLetter")
+ .HasColumnType("text");
+
+ b.Property("MotherId")
+ .HasColumnType("uuid");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Notes")
+ .HasColumnType("text");
+
+ b.Property("PairingCode")
+ .HasColumnType("text");
+
+ b.Property("Provenance")
+ .HasColumnType("text");
+
+ b.Property("TotalBorn")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ExternalRef")
+ .IsUnique()
+ .HasFilter("\"ExternalRef\" IS NOT NULL");
+
+ b.HasIndex("FatherId");
+
+ b.HasIndex("MotherId");
+
+ b.ToTable("Litters");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AppPasswordProtected")
+ .HasColumnType("text");
+
+ b.Property("BackgroundPollEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("Folder")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GmailAddress")
+ .HasColumnType("text");
+
+ b.Property("LastUid")
+ .HasColumnType("bigint");
+
+ b.Property("PollIntervalMinutes")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("MailSettings");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("ab0c0000-0000-0000-0000-000000000001"),
+ BackgroundPollEnabled = false,
+ Folder = "INBOX",
+ LastUid = 0L,
+ PollIntervalMinutes = 15
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Alt")
+ .HasColumnType("text");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Height")
+ .HasColumnType("integer");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Width")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.ToTable("Media");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("SeoDescription")
+ .HasColumnType("text");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Slug")
+ .IsUnique();
+
+ b.ToTable("Pages");
+
+ b.HasData(
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000001"),
+ Slug = "start",
+ Status = "Published",
+ Title = "Startseite"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000002"),
+ Slug = "ueber-die-zucht",
+ Status = "Published",
+ Title = "Über die Zucht"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000003"),
+ Slug = "abgabetiere",
+ Status = "Published",
+ Title = "Abgabetiere"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000004"),
+ Slug = "abgabebedingungen",
+ Status = "Published",
+ Title = "Abgabebedingungen"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000005"),
+ Slug = "farben-genetik",
+ Status = "Published",
+ Title = "Farben & Genetik"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000006"),
+ Slug = "kontakt",
+ Status = "Published",
+ Title = "Kontakt"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000007"),
+ Slug = "impressum",
+ Status = "Published",
+ Title = "Impressum"
+ },
+ new
+ {
+ Id = new Guid("51720001-0000-0000-0000-000000000008"),
+ Slug = "datenschutz",
+ Status = "Published",
+ Title = "Datenschutz"
+ });
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AnsweredAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("AssignedContactId")
+ .HasColumnType("uuid");
+
+ b.Property("BodyText")
+ .HasColumnType("text");
+
+ b.Property("DraftReply")
+ .HasColumnType("text");
+
+ b.Property("FromAddress")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("FromName")
+ .HasColumnType("text");
+
+ b.Property("GmailMessageId")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("InReplyToMessageId")
+ .HasColumnType("text");
+
+ b.Property("ReceivedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReferencesHeader")
+ .HasColumnType("text");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("Subject")
+ .HasColumnType("text");
+
+ b.Property("ThreadId")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("AssignedContactId");
+
+ b.HasIndex("GmailMessageId")
+ .IsUnique();
+
+ b.ToTable("Requests");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.ReturnRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FromContactId")
+ .HasColumnType("uuid");
+
+ b.Property("FromContactName")
+ .HasColumnType("text");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("GerbilName")
+ .HasColumnType("text");
+
+ b.Property("Note")
+ .HasColumnType("text");
+
+ b.Property("OriginalPrice")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.Property("OriginalSaleDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReturnDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ReturnPrice")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("ReturnRecords");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("ContractDate")
+ .HasColumnType("date");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("HandoverDate")
+ .HasColumnType("date");
+
+ b.Property("Price")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ContactId");
+
+ b.ToTable("SaleContracts");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b =>
+ {
+ b.Property("SaleContractId")
+ .HasColumnType("uuid");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("PhotoId")
+ .HasColumnType("uuid");
+
+ b.HasKey("SaleContractId", "GerbilId");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("SaleContractAnimal");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleReservation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("AppointmentDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ContactName")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("GerbilId")
+ .HasColumnType("uuid");
+
+ b.Property("GerbilName")
+ .HasColumnType("text");
+
+ b.Property("HandedOverDate")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Note")
+ .HasColumnType("text");
+
+ b.Property("Price")
+ .HasPrecision(10, 2)
+ .HasColumnType("numeric(10,2)");
+
+ b.Property("ReservedForContactId")
+ .HasColumnType("uuid");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GerbilId");
+
+ b.ToTable("SaleReservations");
+ });
+
+ 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.WaitingListEntry", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("ContactId")
+ .HasColumnType("uuid");
+
+ b.Property("ContactName")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Note")
+ .HasColumnType("text");
+
+ b.Property("RequestedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("WishColor")
+ .HasColumnType("text");
+
+ b.Property("WishGender")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CreatedAt");
+
+ b.ToTable("WaitingListEntries");
+ });
+
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Auth")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Endpoint")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("P256dh")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("WebPushSubscriptions");
+ });
+
+ 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/20260623093055_WebPushSubscriptions.cs b/GerbilManagerWebAPI/Migrations/20260623093055_WebPushSubscriptions.cs
new file mode 100644
index 0000000..18023bd
--- /dev/null
+++ b/GerbilManagerWebAPI/Migrations/20260623093055_WebPushSubscriptions.cs
@@ -0,0 +1,37 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace GerbilManagerWebAPI.Migrations
+{
+ ///
+ public partial class WebPushSubscriptions : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "WebPushSubscriptions",
+ columns: table => new
+ {
+ Id = table.Column(type: "uuid", nullable: false),
+ Endpoint = table.Column(type: "text", nullable: false),
+ P256dh = table.Column(type: "text", nullable: false),
+ Auth = table.Column(type: "text", nullable: false),
+ CreatedAt = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_WebPushSubscriptions", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "WebPushSubscriptions");
+ }
+ }
+}
diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
index 5f38aaf..96e4b77 100644
--- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
+++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs
@@ -1646,6 +1646,32 @@ namespace GerbilManagerWebAPI.Migrations
b.ToTable("WaitingListEntries");
});
+ modelBuilder.Entity("GerbilManagerWebAPI.Models.WebPushSubscription", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid");
+
+ b.Property("Auth")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property