diff --git a/GerbilManager.Tests/AcquisitionEndpointTests.cs b/GerbilManager.Tests/AcquisitionEndpointTests.cs new file mode 100644 index 0000000..47e80df --- /dev/null +++ b/GerbilManager.Tests/AcquisitionEndpointTests.cs @@ -0,0 +1,175 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using GerbilManagerWebAPI.Import; +using GerbilManagerWebAPI.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace GerbilManager.Tests; + +/// +/// ERWERB: acquisition data per animal (purchase date/price/note). +/// - POST creates, GET ?gerbilId= filters, PUT updates, DELETE removes. +/// - CRITICAL: acquisition rows survive the import re-ingest wipe (loose, FK-free +/// GerbilId/SourceContactId), exactly like Feedback. +/// +public class AcquisitionEndpointTests : IClassFixture +{ + private readonly ApiFactory _factory; + public AcquisitionEndpointTests(ApiFactory factory) => _factory = factory; + + [Fact] + public async Task Crud_roundtrip_create_filter_update_delete() + { + var client = _factory.CreateClient(); + var gerbilId = Guid.NewGuid(); + var contactId = Guid.NewGuid(); + + // CREATE + var resp = await client.PostAsJsonAsync("/acquisitions", new + { + gerbilId, + sourceContactId = contactId, + date = "2025-03-14", + price = 25.50m, + note = "Auf der Börse gekauft.", + }); + Assert.Equal(HttpStatusCode.Created, resp.StatusCode); + var created = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement; + var id = created.GetProperty("id").GetString()!; + Assert.Equal(gerbilId.ToString(), created.GetProperty("gerbilId").GetString()); + Assert.Equal(25.50m, created.GetProperty("price").GetDecimal()); + Assert.Equal("2025-03-14", created.GetProperty("date").GetString()); + + // GET ?gerbilId= returns it + var listed = JsonDocument.Parse(await client.GetStringAsync($"/acquisitions?gerbilId={gerbilId}")).RootElement; + Assert.Contains(listed.EnumerateArray(), + a => a.GetProperty("note").GetString() == "Auf der Börse gekauft."); + + // a different gerbilId yields nothing + var other = JsonDocument.Parse(await client.GetStringAsync($"/acquisitions?gerbilId={Guid.NewGuid()}")).RootElement; + Assert.Empty(other.EnumerateArray()); + + // UPDATE + var put = await client.PutAsJsonAsync($"/acquisitions/{id}", new + { + gerbilId, + sourceContactId = (Guid?)null, + date = "2025-04-01", + price = 30m, + note = "Korrigiert.", + }); + Assert.Equal(HttpStatusCode.NoContent, put.StatusCode); + var afterPut = JsonDocument.Parse(await client.GetStringAsync($"/acquisitions/{id}")).RootElement; + Assert.Equal(30m, afterPut.GetProperty("price").GetDecimal()); + Assert.Equal("Korrigiert.", afterPut.GetProperty("note").GetString()); + Assert.True(afterPut.GetProperty("sourceContactId").ValueKind == JsonValueKind.Null); + + // DELETE + var del = await client.DeleteAsync($"/acquisitions/{id}"); + Assert.Equal(HttpStatusCode.NoContent, del.StatusCode); + var gone = await client.GetAsync($"/acquisitions/{id}"); + Assert.Equal(HttpStatusCode.NotFound, gone.StatusCode); + } + + [Fact] + public async Task Acquisition_survives_ingest_wipe() + { + var dir = Path.Combine(Path.GetTempPath(), "acq-ingest-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + var contactId = Guid.NewGuid(); + var fatherId = Guid.NewGuid(); + var motherId = Guid.NewGuid(); + var litterId = Guid.NewGuid(); + + var data = new + { + Contacts = new[] + { + new { Id = contactId, Name = "Test Breeder", Email = "t@e.de", Phone = "", Address = "", Notes = (string?)null, IsBreeder = true, IsReceiver = false, NameSuffix = (string?)null, Provenance = (string?)null } + }, + Litters = new[] + { + new { Id = litterId, Name = "Wurf A", Date = "2026-01-01", TotalBorn = 5, DeathsWithin8Weeks = 0, FatherId = fatherId, MotherId = motherId, ExpectedGoHomeDate = (string?)null, Notes = "", PairingCode = "PC01", ExternalRef = "ext-litter-1", LitterLetter = "A" } + }, + Gerbils = new[] + { + Animal(fatherId, "Papa", "male", contactId), + Animal(motherId, "Mama", "female", contactId), + }, + GerbilPhotos = Array.Empty(), + }; + File.WriteAllText(Path.Combine(dir, "resolved_import.json"), JsonSerializer.Serialize(data)); + + var opts = new DbContextOptionsBuilder() + .UseInMemoryDatabase("acq-ingest-" + Guid.NewGuid().ToString("N")) + .Options; + using var db = new ApplicationContext(opts); + db.Database.EnsureCreated(); + + // An acquisition referencing the gerbil + contact the wipe will delete. + var acqId = Guid.NewGuid(); + db.AcquisitionRecords.Add(new AcquisitionRecord + { + Id = acqId, + GerbilId = fatherId, + SourceContactId = contactId, + Date = new DateOnly(2025, 3, 14), + Price = 25.50m, + Note = "Auf der Börse gekauft.", + CreatedAt = DateTimeOffset.UtcNow, + }); + await db.SaveChangesAsync(); + + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { { "Import:SourcePath", dir } }) + .Build(); + + var result = await new IngestResolvedService(db, config, null!).RunAsync(); + Assert.Contains("Ingestion successful!", result); + + // The acquisition row survives even though the gerbil + contact were wiped/recreated. + var survivor = await db.AcquisitionRecords.SingleAsync(a => a.Id == acqId); + Assert.Equal(fatherId, survivor.GerbilId); + Assert.Equal(contactId, survivor.SourceContactId); + Assert.Equal(25.50m, survivor.Price); + Assert.Equal("Auf der Börse gekauft.", survivor.Note); + Assert.Equal(1, await db.AcquisitionRecords.CountAsync()); + } + finally + { + try { Directory.Delete(dir, recursive: true); } catch { /* best effort */ } + } + } + + private static object Animal(Guid id, string name, string gender, Guid contactId) => new + { + Id = id, + Name = name, + Gender = gender, + Status = "Breeding", + LitterId = (Guid?)null, + OriginContactId = contactId, + ReceiverContactId = (Guid?)null, + EnclosureId = (Guid?)null, + ColorVarietyId = new Guid("00000000-0000-0000-0000-000000000006"), + DateOfBirth = "2025-01-01", + DateOfDeath = (string?)null, + CauseOfDeath = (string?)null, + GoHomeDate = (string?)null, + Genotype = "aa CC DD EE GG PP spsp rere", + Notes = "", + ImportSource = "docx-export", + ExternalRef = "ext-" + name, + RawImportData = "{}", + OriginBreeder = "Test Zucht", + NameSearch = name.ToLowerInvariant(), + CharacterTraits = Array.Empty(), + CharacterNote = (string?)null, + IsDeaf = false, + IsResident = true, + }; +} diff --git a/GerbilManagerWebAPI/ApplicationContext.cs b/GerbilManagerWebAPI/ApplicationContext.cs index a620842..fccaa63 100644 --- a/GerbilManagerWebAPI/ApplicationContext.cs +++ b/GerbilManagerWebAPI/ApplicationContext.cs @@ -25,6 +25,7 @@ public class ApplicationContext : DbContext public DbSet Requests => Set(); public DbSet MailSettings => Set(); public DbSet Feedback => Set(); + public DbSet AcquisitionRecords => Set(); // Keep Gerbil.NameSearch in sync on every save (separator-insensitive search key), // so it can never drift from Name regardless of which code path mutates the entity. @@ -212,6 +213,16 @@ public class ApplicationContext : DbContext e.HasIndex(f => f.CreatedAt); }); + // ERWERB: like Feedback, deliberately relationship-free. GerbilId/SourceContactId + // are plain nullable Guid columns (no navigation properties → EF creates NO foreign + // key), so the import re-ingest wipe of Gerbils/Contacts never cascades into — or + // breaks — acquisition rows. They survive re-ingest, which is the whole point. + modelBuilder.Entity(e => + { + e.Property(a => a.Price).HasPrecision(10, 2); + e.HasIndex(a => a.GerbilId); + }); + // DB-4: German collation on remaining searched/sorted text columns (Npgsql-only). if (isNpgsql) { diff --git a/GerbilManagerWebAPI/Dtos/AcquisitionDtos.cs b/GerbilManagerWebAPI/Dtos/AcquisitionDtos.cs new file mode 100644 index 0000000..21eeb2b --- /dev/null +++ b/GerbilManagerWebAPI/Dtos/AcquisitionDtos.cs @@ -0,0 +1,20 @@ +namespace GerbilManagerWebAPI.Dtos +{ + /// ERWERB: payload for POST/PUT /acquisitions (acquisition data per animal). + public record AcquisitionInput( + Guid? GerbilId, + Guid? SourceContactId, + DateOnly? Date, + decimal? Price, + string? Note); + + /// ERWERB: response DTO for a stored acquisition record. + public record AcquisitionDto( + Guid Id, + Guid? GerbilId, + Guid? SourceContactId, + DateOnly? Date, + decimal? Price, + string? Note, + DateTimeOffset CreatedAt); +} diff --git a/GerbilManagerWebAPI/Endpoints/AcquisitionEndpoints.cs b/GerbilManagerWebAPI/Endpoints/AcquisitionEndpoints.cs new file mode 100644 index 0000000..242b80b --- /dev/null +++ b/GerbilManagerWebAPI/Endpoints/AcquisitionEndpoints.cs @@ -0,0 +1,89 @@ +using GerbilManagerWebAPI.Dtos; +using GerbilManagerWebAPI.Models; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.EntityFrameworkCore; + +namespace GerbilManagerWebAPI.Endpoints +{ + /// + /// ERWERB: acquisition data per animal (purchase date/price/note). + /// GET /acquisitions?gerbilId=… -> records for one animal (or all), newest date first. + /// GET /acquisitions/{id} -> a single record. + /// POST /acquisitions -> create, returns 201. + /// PUT /acquisitions/{id} -> update, returns 204. + /// DELETE /acquisitions/{id} -> delete, returns 204. + /// Decoupled from gerbils/contacts (loose nullable Guid columns, no FK), so rows survive + /// the import re-ingest wipe (mirrors Feedback). + /// + public static class AcquisitionEndpoints + { + public static IEndpointRouteBuilder MapAcquisitionEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/acquisitions").WithTags("Acquisitions"); + + group.MapGet("/", async (ApplicationContext db, Guid? gerbilId) => + { + var rows = await db.AcquisitionRecords.AsNoTracking() + .Where(a => gerbilId == null || a.GerbilId == gerbilId) + .ToListAsync(); + // Order in memory: SQLite (test host) cannot ORDER BY a DateTimeOffset column, + // and DateOnly ordering stays consistent across providers this way. + return TypedResults.Ok(rows + .OrderByDescending(a => a.Date ?? DateOnly.MinValue) + .ThenByDescending(a => a.CreatedAt) + .Select(ToDto) + .ToList()); + }); + + group.MapGet("/{id:guid}", async Task, NotFound>> (Guid id, ApplicationContext db) => + { + var a = await db.AcquisitionRecords.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); + return a is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(a)); + }); + + group.MapPost("/", async (AcquisitionInput input, ApplicationContext db) => + { + var a = new AcquisitionRecord + { + Id = Guid.NewGuid(), + GerbilId = input.GerbilId, + SourceContactId = input.SourceContactId, + Date = input.Date, + Price = input.Price, + Note = string.IsNullOrWhiteSpace(input.Note) ? null : input.Note.Trim(), + CreatedAt = DateTimeOffset.UtcNow, + }; + db.AcquisitionRecords.Add(a); + await db.SaveChangesAsync(); + return TypedResults.Created($"/acquisitions/{a.Id}", ToDto(a)); + }); + + group.MapPut("/{id:guid}", async Task> (Guid id, AcquisitionInput input, ApplicationContext db) => + { + var a = await db.AcquisitionRecords.FirstOrDefaultAsync(x => x.Id == id); + if (a is null) return TypedResults.NotFound(); + a.GerbilId = input.GerbilId; + a.SourceContactId = input.SourceContactId; + a.Date = input.Date; + a.Price = input.Price; + a.Note = string.IsNullOrWhiteSpace(input.Note) ? null : input.Note.Trim(); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + }); + + group.MapDelete("/{id:guid}", async Task> (Guid id, ApplicationContext db) => + { + var a = await db.AcquisitionRecords.FirstOrDefaultAsync(x => x.Id == id); + if (a is null) return TypedResults.NotFound(); + db.AcquisitionRecords.Remove(a); + await db.SaveChangesAsync(); + return TypedResults.NoContent(); + }); + + return app; + } + + private static AcquisitionDto ToDto(AcquisitionRecord a) => + new(a.Id, a.GerbilId, a.SourceContactId, a.Date, a.Price, a.Note, a.CreatedAt); + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260622201453_AddAcquisitionRecord.Designer.cs b/GerbilManagerWebAPI/Migrations/20260622201453_AddAcquisitionRecord.Designer.cs new file mode 100644 index 0000000..cf3ad0b --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260622201453_AddAcquisitionRecord.Designer.cs @@ -0,0 +1,1580 @@ +// +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("20260622201453_AddAcquisitionRecord")] + partial class AddAcquisitionRecord + { + /// + 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 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.Property("Provenance") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Contacts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Enclosures"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EnclosureId"); + + b.ToTable("EnclosurePhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Feedback", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ClientTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("Context") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EntityName") + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("Url") + .HasColumnType("text"); + + b.Property("UserAgent") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.ToTable("Feedback"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CauseOfDeath") + .HasColumnType("text"); + + b.Property("CharacterNote") + .HasColumnType("text"); + + b.Property("CharacterTraits") + .IsRequired() + .HasColumnType("text"); + + b.Property("ColorVarietyId") + .HasColumnType("uuid"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("DateOfDeath") + .HasColumnType("date"); + + b.Property("EnclosureId") + .HasColumnType("uuid"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text"); + + b.Property("Genotype") + .HasColumnType("text"); + + b.Property("GoHomeDate") + .HasColumnType("date"); + + b.Property("ImportSource") + .HasColumnType("text"); + + b.Property("IsCastrated") + .HasColumnType("boolean"); + + b.Property("IsDeaf") + .HasColumnType("boolean"); + + b.Property("IsResident") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("LitterId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("NameSearch") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OriginBreeder") + .HasColumnType("text") + .UseCollation("de-x-icu"); + + b.Property("OriginContactId") + .HasColumnType("uuid"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("RawImportData") + .HasColumnType("text"); + + b.Property("ReceiverContactId") + .HasColumnType("uuid"); + + b.Property("SpottingType") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ColorVarietyId"); + + b.HasIndex("EnclosureId"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("LitterId"); + + b.HasIndex("OriginContactId"); + + b.HasIndex("ReceiverContactId"); + + b.ToTable("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Caption") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("SortOrder") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("GerbilPhotos"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("Veterinarian") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("HealthRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("DeathsWithin8Weeks") + .HasColumnType("integer"); + + b.Property("ExpectedGoHomeDate") + .HasColumnType("date"); + + b.Property("ExternalRef") + .HasColumnType("text"); + + b.Property("FatherId") + .HasColumnType("uuid"); + + b.Property("LitterLetter") + .HasColumnType("text"); + + b.Property("MotherId") + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("PairingCode") + .HasColumnType("text"); + + b.Property("Provenance") + .HasColumnType("text"); + + b.Property("TotalBorn") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ExternalRef") + .IsUnique() + .HasFilter("\"ExternalRef\" IS NOT NULL"); + + b.HasIndex("FatherId"); + + b.HasIndex("MotherId"); + + b.ToTable("Litters"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.MailSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AppPasswordProtected") + .HasColumnType("text"); + + b.Property("BackgroundPollEnabled") + .HasColumnType("boolean"); + + b.Property("Folder") + .IsRequired() + .HasColumnType("text"); + + b.Property("GmailAddress") + .HasColumnType("text"); + + b.Property("LastUid") + .HasColumnType("bigint"); + + b.Property("PollIntervalMinutes") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("MailSettings"); + + b.HasData( + new + { + Id = new Guid("ab0c0000-0000-0000-0000-000000000001"), + BackgroundPollEnabled = false, + Folder = "INBOX", + LastUid = 0L, + PollIntervalMinutes = 15 + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Media", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Alt") + .HasColumnType("text"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Height") + .HasColumnType("integer"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.Property("Width") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Media"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("SeoDescription") + .HasColumnType("text"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Slug") + .IsUnique(); + + b.ToTable("Pages"); + + b.HasData( + new + { + Id = new Guid("51720001-0000-0000-0000-000000000001"), + Slug = "start", + Status = "Published", + Title = "Startseite" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000002"), + Slug = "ueber-die-zucht", + Status = "Published", + Title = "Über die Zucht" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000003"), + Slug = "abgabetiere", + Status = "Published", + Title = "Abgabetiere" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000004"), + Slug = "abgabebedingungen", + Status = "Published", + Title = "Abgabebedingungen" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000005"), + Slug = "farben-genetik", + Status = "Published", + Title = "Farben & Genetik" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000006"), + Slug = "kontakt", + Status = "Published", + Title = "Kontakt" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000007"), + Slug = "impressum", + Status = "Published", + Title = "Impressum" + }, + new + { + Id = new Guid("51720001-0000-0000-0000-000000000008"), + Slug = "datenschutz", + Status = "Published", + Title = "Datenschutz" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AnsweredAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AssignedContactId") + .HasColumnType("uuid"); + + b.Property("BodyText") + .HasColumnType("text"); + + b.Property("DraftReply") + .HasColumnType("text"); + + b.Property("FromAddress") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromName") + .HasColumnType("text"); + + b.Property("GmailMessageId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InReplyToMessageId") + .HasColumnType("text"); + + b.Property("ReceivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ReferencesHeader") + .HasColumnType("text"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("Subject") + .HasColumnType("text"); + + b.Property("ThreadId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AssignedContactId"); + + b.HasIndex("GmailMessageId") + .IsUnique(); + + b.ToTable("Requests"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ContactId") + .HasColumnType("uuid"); + + b.Property("ContractDate") + .HasColumnType("date"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HandoverDate") + .HasColumnType("date"); + + b.Property("Price") + .HasPrecision(10, 2) + .HasColumnType("numeric(10,2)"); + + b.HasKey("Id"); + + b.HasIndex("ContactId"); + + b.ToTable("SaleContracts"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.Property("SaleContractId") + .HasColumnType("uuid"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("PhotoId") + .HasColumnType("uuid"); + + b.HasKey("SaleContractId", "GerbilId"); + + b.HasIndex("GerbilId"); + + b.ToTable("SaleContractAnimal"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Site", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("DefaultLocale") + .IsRequired() + .HasColumnType("text"); + + b.Property("NavOrder") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sites"); + + b.HasData( + new + { + Id = new Guid("5172e000-0000-0000-0000-000000000001"), + DefaultLocale = "de", + NavOrder = "[\"51720001-0000-0000-0000-000000000001\",\"51720001-0000-0000-0000-000000000002\",\"51720001-0000-0000-0000-000000000003\",\"51720001-0000-0000-0000-000000000004\",\"51720001-0000-0000-0000-000000000005\",\"51720001-0000-0000-0000-000000000006\"]" + }); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("date"); + + b.Property("GerbilId") + .HasColumnType("uuid"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("WeightGrams") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GerbilId"); + + b.ToTable("WeightRecords"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Block", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Page", null) + .WithMany("Blocks") + .HasForeignKey("PageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.EnclosurePhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", null) + .WithMany() + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Gerbil", b => + { + b.HasOne("GerbilManagerWebAPI.Models.ColorVariety", "ColorVariety") + .WithMany() + .HasForeignKey("ColorVarietyId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Enclosure", "Enclosure") + .WithMany("Gerbils") + .HasForeignKey("EnclosureId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Litter", "Litter") + .WithMany() + .HasForeignKey("LitterId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "OriginContact") + .WithMany() + .HasForeignKey("OriginContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Contact", "ReceiverContact") + .WithMany() + .HasForeignKey("ReceiverContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("ColorVariety"); + + b.Navigation("Enclosure"); + + b.Navigation("Litter"); + + b.Navigation("OriginContact"); + + b.Navigation("ReceiverContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.GerbilPhoto", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.HealthRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Litter", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Father") + .WithMany() + .HasForeignKey("FatherId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Mother") + .WithMany() + .HasForeignKey("MotherId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Father"); + + b.Navigation("Mother"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Request", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "AssignedContact") + .WithMany() + .HasForeignKey("AssignedContactId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("AssignedContact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Contact", "Contact") + .WithMany() + .HasForeignKey("ContactId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Contact"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContractAnimal", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", "Gerbil") + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GerbilManagerWebAPI.Models.SaleContract", null) + .WithMany("Animals") + .HasForeignKey("SaleContractId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Gerbil"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.WeightRecord", b => + { + b.HasOne("GerbilManagerWebAPI.Models.Gerbil", null) + .WithMany() + .HasForeignKey("GerbilId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Enclosure", b => + { + b.Navigation("Gerbils"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.Page", b => + { + b.Navigation("Blocks"); + }); + + modelBuilder.Entity("GerbilManagerWebAPI.Models.SaleContract", b => + { + b.Navigation("Animals"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/20260622201453_AddAcquisitionRecord.cs b/GerbilManagerWebAPI/Migrations/20260622201453_AddAcquisitionRecord.cs new file mode 100644 index 0000000..fdc712a --- /dev/null +++ b/GerbilManagerWebAPI/Migrations/20260622201453_AddAcquisitionRecord.cs @@ -0,0 +1,44 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GerbilManagerWebAPI.Migrations +{ + /// + public partial class AddAcquisitionRecord : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AcquisitionRecords", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + GerbilId = table.Column(type: "uuid", nullable: true), + SourceContactId = table.Column(type: "uuid", nullable: true), + Date = table.Column(type: "date", nullable: true), + Price = table.Column(type: "numeric(10,2)", precision: 10, scale: 2, nullable: true), + Note = table.Column(type: "text", nullable: true), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AcquisitionRecords", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_AcquisitionRecords_GerbilId", + table: "AcquisitionRecords", + column: "GerbilId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AcquisitionRecords"); + } + } +} diff --git a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs index 725b04b..37c8d82 100644 --- a/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs +++ b/GerbilManagerWebAPI/Migrations/ApplicationContextModelSnapshot.cs @@ -21,6 +21,38 @@ namespace GerbilManagerWebAPI.Migrations 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") diff --git a/GerbilManagerWebAPI/Models/AcquisitionRecord.cs b/GerbilManagerWebAPI/Models/AcquisitionRecord.cs new file mode 100644 index 0000000..0330254 --- /dev/null +++ b/GerbilManagerWebAPI/Models/AcquisitionRecord.cs @@ -0,0 +1,38 @@ +using System.ComponentModel.DataAnnotations; + +namespace GerbilManagerWebAPI.Models +{ + /// + /// ERWERB: how/when an animal was acquired (purchase date, price, note) — sourced from + /// RennmausPro's herktier_tb (_TID/_DATE/_PRICE/_BEM). The seller is already linked + /// via Gerbil.OriginContactId; this record adds the date/price/note around that. + /// + /// Decoupled from the rest of the model ON PURPOSE — GerbilId/SourceContactId are plain + /// nullable Guid columns (NOT enforced foreign keys), so the import re-ingest wipe + /// (IngestResolvedService, which deletes Gerbils/Contacts) never cascades into — or + /// breaks — acquisition rows. They survive re-ingest, mirroring the Feedback pattern. + /// + public class AcquisitionRecord + { + [Key] + public Guid Id { get; set; } + + /// Loose reference (no FK) to the acquired gerbil. + public Guid? GerbilId { get; set; } + + /// Loose reference (no FK) to the source/seller contact, if known. + public Guid? SourceContactId { get; set; } + + /// When the animal was acquired (purchase date). + public DateOnly? Date { get; set; } + + /// Acquisition price (purchase cost), if recorded. + public decimal? Price { get; set; } + + /// Free-text note about the acquisition. + public string? Note { get; set; } + + /// Server-side creation time. + public DateTimeOffset CreatedAt { get; set; } + } +} diff --git a/GerbilManagerWebAPI/Program.cs b/GerbilManagerWebAPI/Program.cs index ecb8493..d93b9d5 100644 --- a/GerbilManagerWebAPI/Program.cs +++ b/GerbilManagerWebAPI/Program.cs @@ -127,6 +127,7 @@ app.MapCmsEndpoints(); app.MapRequestEndpoints(); app.MapNamesEndpoints(); app.MapFeedbackEndpoints(); +app.MapAcquisitionEndpoints(); app.Run(); diff --git a/gerbil-manager-web/e2e/erwerb.spec.ts b/gerbil-manager-web/e2e/erwerb.spec.ts new file mode 100644 index 0000000..c93ed8e --- /dev/null +++ b/gerbil-manager-web/e2e/erwerb.spec.ts @@ -0,0 +1,45 @@ +/** ERWERB: Erwerb/Kauf je Tier — Sektion in der Rennmausakte (anlegen / bearbeiten / löschen). */ +import { de, expect, skipUnlessMock, test } from './fixtures' + +const t = de.pages.tierTabs.acquisition + +test('Tierakte: Erwerb erfassen, bearbeiten und löschen', async ({ page, mockDb }) => { + skipUnlessMock() + await page.goto('/rennmaeuse/kruemel') + + const section = page.locator('section.ak-card', { hasText: t.sectionTitle }) + await expect(section).toBeVisible() + await expect(section).toContainText(t.empty) + + // Anlegen: Formular öffnen, Felder füllen, speichern. + await section.getByRole('button', { name: t.addButton }).click() + await section.getByLabel(t.fields.date).fill('2025-03-14') + await section.getByLabel(t.fields.price).fill('25.50') + await section.getByLabel(t.fields.note).fill('Auf der Börse gekauft.') + await section.getByRole('button', { name: t.addButton }).click() + + // Eintrag erscheint in der Liste, im Mock gespeichert. + await expect(section).toContainText('25,50') + await expect(section).toContainText('Auf der Börse gekauft.') + expect(mockDb).not.toBeNull() + expect(mockDb!.acquisitions.length).toBe(1) + expect(mockDb!.acquisitions[0]).toMatchObject({ + gerbilId: 'kruemel', + date: '2025-03-14', + price: 25.5, + note: 'Auf der Börse gekauft.', + }) + + // Bearbeiten: Preis ändern. + await section.getByRole('button', { name: t.edit }).click() + await section.getByLabel(t.fields.price).fill('30') + await section.getByRole('button', { name: t.saveButton }).click() + await expect(section).toContainText('30,00') + expect(mockDb!.acquisitions[0]).toMatchObject({ price: 30 }) + + // Löschen (window.confirm bestätigen). + page.once('dialog', (d) => d.accept()) + await section.getByRole('button', { name: t.delete }).click() + await expect(section).toContainText(t.empty) + expect(mockDb!.acquisitions.length).toBe(0) +}) diff --git a/gerbil-manager-web/e2e/mock-api.ts b/gerbil-manager-web/e2e/mock-api.ts index 78faa7a..82c1b67 100644 --- a/gerbil-manager-web/e2e/mock-api.ts +++ b/gerbil-manager-web/e2e/mock-api.ts @@ -416,6 +416,50 @@ export async function installMockApi(page: Page): Promise { return json(route, 405) } + // ERWERB: Erwerb/Kauf je Tier — GET ?gerbilId= (Array, kein Gridify-Paging), + // POST/PUT/DELETE. Vor den generischen Kollektionen, weil GET kein {items}-Objekt + // liefert und nach gerbilId statt Gridify-filter selektiert. + const acqMatch = path.match(/^\/acquisitions(?:\/([^/]+))?$/) + if (acqMatch) { + const acqId = acqMatch[1] ? decodeURIComponent(acqMatch[1]) : null + if (!acqId) { + if (method === 'GET') { + const gid = url.searchParams.get('gerbilId') + const rows = db.acquisitions.filter((a) => !gid || a.gerbilId === gid) + return json(route, 200, [...rows].reverse()) + } + if (method === 'POST') { + const created = { + id: newId('acq'), + sourceContactId: null, + date: null, + price: null, + note: null, + ...(request.postDataJSON() as Row), + createdAt: new Date().toISOString(), + } + db.acquisitions.push(created) + return json(route, 201, created) + } + return json(route, 405) + } + const ai = db.acquisitions.findIndex((a) => a.id === acqId) + if (method === 'GET') { + return ai >= 0 ? json(route, 200, db.acquisitions[ai]) : json(route, 404, { title: 'Not Found' }) + } + if (method === 'PUT') { + if (ai < 0) return json(route, 404, { title: 'Not Found' }) + Object.assign(db.acquisitions[ai], request.postDataJSON() as Row) + return json(route, 204) + } + if (method === 'DELETE') { + if (ai < 0) return json(route, 404, { title: 'Not Found' }) + db.acquisitions.splice(ai, 1) + return json(route, 204) + } + return json(route, 405) + } + // Generische Kollektionen: / und // m = path.match(/^\/([a-z-]+)(?:\/([^/]+))?$/) const col = m ? collections[m[1]] : undefined diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index b07cefe..3481501 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -78,6 +78,8 @@ export interface MockDb { namesConfigured: boolean // FEEDBACK: "Fehler melden" — gesammelte Berichte (POST /feedback) feedback: Record[] + // ERWERB: Erwerb/Kauf je Tier (Kaufdatum, Preis, Notiz) — /acquisitions + acquisitions: Record[] } function gerbil( @@ -376,5 +378,6 @@ export function seedDb(): MockDb { saleAdConfigured: true, namesConfigured: true, feedback: [], + acquisitions: [], } } diff --git a/gerbil-manager-web/src/api/acquisitions.ts b/gerbil-manager-web/src/api/acquisitions.ts new file mode 100644 index 0000000..1408a04 --- /dev/null +++ b/gerbil-manager-web/src/api/acquisitions.ts @@ -0,0 +1,45 @@ +/** + * ERWERB: typed API client for the acquisition (AcquisitionRecord) resource. + * Captures when/how an animal was acquired + its price. Decoupled from the gerbil + * (loose nullable ids, no FK) so rows survive the import re-ingest wipe. + */ +import { api } from './client' +import type { DateOnlyString } from './types' + +const RESOURCE = '/acquisitions' + +export interface Acquisition { + id: string + gerbilId: string | null + sourceContactId: string | null + date: DateOnlyString | null + price: number | null + note: string | null + createdAt: string +} + +/** Payload for POST/PUT /acquisitions. */ +export interface AcquisitionInput { + gerbilId?: string | null + sourceContactId?: string | null + date?: DateOnlyString | null + price?: number | null + note?: string | null +} + +/** All acquisition records for one animal (newest acquisition date first). */ +export function listAcquisitions(gerbilId: string): Promise { + return api.get(`${RESOURCE}?gerbilId=${encodeURIComponent(gerbilId)}`) +} + +export function createAcquisition(body: AcquisitionInput): Promise { + return api.post(RESOURCE, body) +} + +export function updateAcquisition(id: string, body: AcquisitionInput): Promise { + return api.put(`${RESOURCE}/${id}`, body) +} + +export function deleteAcquisition(id: string): Promise { + return api.delete(`${RESOURCE}/${id}`) +} diff --git a/gerbil-manager-web/src/components/GerbilAcquisitionSection.tsx b/gerbil-manager-web/src/components/GerbilAcquisitionSection.tsx new file mode 100644 index 0000000..e9e0144 --- /dev/null +++ b/gerbil-manager-web/src/components/GerbilAcquisitionSection.tsx @@ -0,0 +1,204 @@ +/** + * ERWERB: compact, self-contained acquisition section for the Rennmausakte. + * Shows when/at what price an animal was acquired (purchase date, price, note), + * with inline add / edit / delete. Backed by /acquisitions (loose nullable ids, + * no FK) so rows survive the import re-ingest wipe. + * + * Embedded with a single line in GerbilDetailPage. + */ +import { useState, type FormEvent } from 'react' +import { de } from '../strings/de' +import { + createAcquisition, + deleteAcquisition, + listAcquisitions, + updateAcquisition, + type Acquisition, +} from '../api/acquisitions' +import { useApi, useMutation } from '../hooks/useApi' +import { formatDate } from '../format/labels' + +function formatPrice(price: number): string { + return `${price.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €` +} + +export default function GerbilAcquisitionSection({ gerbilId }: { gerbilId: string }) { + const t = de.pages.tierTabs.acquisition + const records = useApi(() => listAcquisitions(gerbilId), [gerbilId]) + + const [editing, setEditing] = useState(null) // null = none, '' = new + const [date, setDate] = useState('') + const [price, setPrice] = useState('') + const [note, setNote] = useState('') + const [error, setError] = useState(null) + + const save = useMutation((id: string | '') => { + const body = { + gerbilId, + date: date || null, + price: price.trim() === '' ? null : Number(price), + note: note.trim() || null, + } + return id === '' ? createAcquisition(body) : updateAcquisition(id, body) + }) + const removal = useMutation((id: string) => deleteAcquisition(id)) + + function startNew() { + setEditing('') + setDate('') + setPrice('') + setNote('') + setError(null) + } + + function startEdit(a: Acquisition) { + setEditing(a.id) + setDate(a.date ?? '') + setPrice(a.price === null ? '' : String(a.price)) + setNote(a.note ?? '') + setError(null) + } + + function cancel() { + setEditing(null) + setError(null) + } + + async function onSubmit(e: FormEvent) { + e.preventDefault() + const trimmedNote = note.trim() + if (!date && price.trim() === '' && trimmedNote === '') { + setError(t.validation.empty) + return + } + if (price.trim() !== '') { + const value = Number(price) + if (Number.isNaN(value) || value < 0 || value > 9999) { + setError(t.validation.priceRange) + return + } + } + setError(null) + const r = await save.run(editing ?? '') + if (r.ok) { + setEditing(null) + records.reload() + } + } + + async function onDelete(id: string) { + if (!window.confirm(t.deleteConfirm)) return + await removal.run(id) + records.reload() + } + + const items = records.data ?? [] + + return ( +
+

{t.sectionTitle}

+

+ {t.intro} +

+ + {records.loading &&

{de.common.loading}

} + {records.error && ( +
+ {records.error} + +
+ )} + {removal.error &&
{removal.error}
} + + {!records.loading && !records.error && items.length === 0 && editing === null && ( +

{t.empty}

+ )} + + {items.length > 0 && ( +
    + {items.map((a) => ( +
  • + {a.date ? formatDate(a.date) : t.noDate} + {a.price === null ? t.noPrice : formatPrice(a.price)} + {a.note && {a.note}} + + +
  • + ))} +
+ )} + + {editing !== null ? ( +
+

{editing === '' ? t.addTitle : t.editTitle}

+
+ + + +
+
+ + +
+ {error && {error}} + {save.error &&
{save.error}
} +
+ ) : ( +
+ +
+ )} +
+ ) +} diff --git a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx index 8aa26db..e97b834 100644 --- a/gerbil-manager-web/src/pages/GerbilDetailPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilDetailPage.tsx @@ -10,6 +10,7 @@ import { ALL_TRAITS, TRAIT_CATEGORIES } from '../format/traits' import { fromDisplayString, genotypeToFarbschlag, displayGenotypeSafe } from '../genetics' import type { Gender, GerbilStatus } from '../api/types' import FarbschlagImage from '../components/FarbschlagImage' +import GerbilAcquisitionSection from '../components/GerbilAcquisitionSection' import GerbilHealthTab from '../components/GerbilHealthTab' import GerbilPhotosTab from '../components/GerbilPhotosTab' import GerbilProfilePhoto from '../components/GerbilProfilePhoto' @@ -336,6 +337,8 @@ export default function GerbilDetailPage() { + +

{t.detail.genetics}

{geno ? ( diff --git a/gerbil-manager-web/src/strings/de.ts b/gerbil-manager-web/src/strings/de.ts index a146723..01071b4 100644 --- a/gerbil-manager-web/src/strings/de.ts +++ b/gerbil-manager-web/src/strings/de.ts @@ -731,6 +731,32 @@ export const de = { fileRequired: 'Bitte zuerst ein Foto auswählen.', }, }, + // ERWERB: Erwerb/Kauf je Tier (Kaufdatum, Preis, Notiz) — eigene Sektion in der Akte. + acquisition: { + sectionTitle: 'Erwerb', + intro: 'Wann und zu welchem Preis dieses Tier erworben wurde.', + addTitle: 'Erwerb erfassen', + editTitle: 'Erwerb bearbeiten', + fields: { + date: 'Kaufdatum', + price: 'Preis (€)', + note: 'Notiz', + }, + addButton: 'Hinzufügen', + saveButton: 'Speichern', + saving: 'Speichern …', + cancel: 'Abbrechen', + edit: 'Bearbeiten', + delete: 'Löschen', + empty: 'Noch keine Erwerbsdaten erfasst.', + noDate: 'Ohne Datum', + noPrice: '—', + deleteConfirm: 'Diesen Erwerbseintrag wirklich löschen?', + validation: { + empty: 'Bitte mindestens Kaufdatum, Preis oder Notiz angeben.', + priceRange: 'Bitte einen plausiblen Preis (0 bis 9999 €) angeben.', + }, + }, }, // ── EXPORT-1 (Oscar): Datenexport (Karte auf /einstellungen) ── datenexport: {