feat(tiere): Eltern direkt in der Tier-Akte bearbeiten (QOL)
Im Tier-Formular gibt es jetzt den Block „Abstammung" mit Wurf-Auswahl und
Vater-/Mutter-Picker — die Eltern müssen nicht mehr über die Wurf-Seite
gesucht und dort editiert werden. Das Datenmodell bleibt unverändert: Eltern
hängen weiterhin am Geburtswurf.
- Backend: PUT /gerbils/{id}/parents schreibt Litter.FatherId/MotherId des
Geburtswurfs. Ohne Wurf wird ein bestehender mit gleichem Elternpaar +
gleichem Datum verknüpft, sonst ein Träger-Wurf angelegt
("Wurf von X + Y", ShowInChronicle=false, IsManual=true).
Geschlechts-Regel wiederverwendet LitterEndpoints.ValidateParents,
Selbstbezug (Tier als eigener Elternteil) wird abgewiesen.
- UI: Vorbelegung aus dem gewählten Wurf, Warnung mit Anzahl der Geschwister
(Eltern gehören dem Wurf → Änderung gilt für alle), Hinweis wenn ein
Wurf-Eintrag angelegt wird. Texte in de.ts.
- Nebenbei: Speichern nutzt im Edit-Modus die Route-Id (PUT /gerbils/{id}
antwortet 204 ohne Body) und der vorher schon rote Spec-Locator
„Würfe als Elternteil" ist auf den Abschnitt eingegrenzt.
- Tests: GerbilParentsTests (9), e2e tiere.spec (2 neu) + Mock-Route.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using GerbilManagerWebAPI.Common;
|
||||
using GerbilManagerWebAPI.Common;
|
||||
using GerbilManagerWebAPI.Dtos;
|
||||
using GerbilManagerWebAPI.Import;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
@@ -90,6 +90,65 @@ namespace GerbilManagerWebAPI.Endpoints
|
||||
return TypedResults.NoContent();
|
||||
});
|
||||
|
||||
// PUT /gerbils/{id}/parents — QOL (Wunsch der Züchterin): Eltern DIREKT in der
|
||||
// Tier-Akte pflegen, ohne vorher den Wurf suchen zu müssen. Das Datenmodell bleibt
|
||||
// unverändert — Eltern hängen weiterhin am GEBURTSWURF (Litter.FatherId/MotherId),
|
||||
// dieser Endpoint schreibt nur genau dorthin:
|
||||
// • Tier hat einen Wurf → dessen Eltern werden gesetzt. Das gilt zwangsläufig für
|
||||
// ALLE Jungtiere des Wurfs; die UI warnt vorher mit SiblingCount.
|
||||
// • Tier hat keinen Wurf → gibt es einen Wurf mit genau diesem Elternpaar UND
|
||||
// demselben Datum wie das Geburtsdatum (= dieselbe Geburt), wird das Tier dort
|
||||
// eingehängt (LitterAttached). Sonst wird ein Wurf angelegt (LitterCreated) mit
|
||||
// ShowInChronicle=false (nicht in der Wurfchronik, nur zur Abstammung) und
|
||||
// IsManual=true (überlebt den Re-Ingest).
|
||||
group.MapPut("/{id:guid}/parents", async Task<Results<Ok<GerbilParentsResult>, NotFound, BadRequest<ParentGenderError>, BadRequest<string>>> (
|
||||
Guid id, GerbilParentsInput input, ApplicationContext db) =>
|
||||
{
|
||||
var g = await db.Gerbils.FirstOrDefaultAsync(x => x.Id == id);
|
||||
if (g is null) return TypedResults.NotFound();
|
||||
if (input.FatherId == id || input.MotherId == id)
|
||||
return TypedResults.BadRequest("Ein Tier kann nicht sein eigener Elternteil sein.");
|
||||
var genderErr = await LitterEndpoints.ValidateParents(db, input.FatherId, input.MotherId);
|
||||
if (genderErr is not null) return TypedResults.BadRequest(genderErr);
|
||||
|
||||
var litter = g.LitterId is Guid lid
|
||||
? await db.Litters.FirstOrDefaultAsync(l => l.Id == lid)
|
||||
: null;
|
||||
|
||||
// Nichts zu tun: kein Wurf und keine Eltern angegeben.
|
||||
if (litter is null && input.FatherId is null && input.MotherId is null)
|
||||
return TypedResults.Ok(new GerbilParentsResult(null, null, false, false, 0, null, null));
|
||||
|
||||
var created = false;
|
||||
var attached = false;
|
||||
if (litter is null)
|
||||
{
|
||||
// Gleiches Elternpaar + gleiches Datum ⇒ dieselbe Geburt: an den bestehenden
|
||||
// Wurf hängen (macht das Tier korrekt zum Geschwister), statt zu duplizieren.
|
||||
if (g.DateOfBirth is DateOnly dob)
|
||||
{
|
||||
litter = await db.Litters.FirstOrDefaultAsync(l =>
|
||||
l.FatherId == input.FatherId && l.MotherId == input.MotherId && l.Date == dob);
|
||||
attached = litter is not null;
|
||||
}
|
||||
litter ??= NewParentLitter(g, await NameOfAsync(db, input.FatherId), await NameOfAsync(db, input.MotherId));
|
||||
if (!attached)
|
||||
{
|
||||
db.Litters.Add(litter);
|
||||
created = true;
|
||||
}
|
||||
g.LitterId = litter.Id;
|
||||
}
|
||||
|
||||
litter.FatherId = input.FatherId;
|
||||
litter.MotherId = input.MotherId;
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var siblingCount = await db.Gerbils.CountAsync(x => x.LitterId == litter.Id && x.Id != id);
|
||||
return TypedResults.Ok(new GerbilParentsResult(
|
||||
litter.Id, litter.Name, created, attached, siblingCount, litter.FatherId, litter.MotherId));
|
||||
});
|
||||
|
||||
// DELETE /gerbils/{id} (409 if referenced as a litter parent)
|
||||
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound, Conflict<string>>> (Guid id, ApplicationContext db) =>
|
||||
{
|
||||
@@ -110,6 +169,24 @@ namespace GerbilManagerWebAPI.Endpoints
|
||||
return app;
|
||||
}
|
||||
|
||||
private static async Task<string?> NameOfAsync(ApplicationContext db, Guid? gerbilId) =>
|
||||
gerbilId is Guid gid
|
||||
? await db.Gerbils.AsNoTracking().Where(x => x.Id == gid).Select(x => x.Name).FirstOrDefaultAsync()
|
||||
: null;
|
||||
|
||||
/// <summary>Träger-Wurf für die Eltern eines Tiers ohne Geburtswurf. Namensschema wie die
|
||||
/// vom Import erzeugten Stammbaum-Würfe ("Wurf von X + Y"); ShowInChronicle=false, damit die
|
||||
/// Wurfchronik nicht mit Hilfs-Würfen zuwächst; IsManual=true, damit der Re-Ingest ihn nicht wegräumt.</summary>
|
||||
private static Litter NewParentLitter(Gerbil g, string? fatherName, string? motherName) => new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = $"Wurf von {fatherName ?? "—"} + {motherName ?? "—"}",
|
||||
Date = g.DateOfBirth,
|
||||
ShowInChronicle = false,
|
||||
IsManual = true,
|
||||
Notes = $"Automatisch angelegt, um die Eltern von {g.Name} zu tragen.",
|
||||
};
|
||||
|
||||
// CR-2 FIX: PATCH semantics — every omitted/null field keeps the stored value.
|
||||
// Prevents silent data loss when the frontend sends partial bodies (ForSale toggle,
|
||||
// Charakterbogen save, any partial updateGerbil call). On create, supply safe defaults
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using GerbilManagerWebAPI.Common;
|
||||
using GerbilManagerWebAPI.Common;
|
||||
using GerbilManagerWebAPI.Dtos;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using Gridify;
|
||||
@@ -65,7 +65,9 @@ namespace GerbilManagerWebAPI.Endpoints
|
||||
}
|
||||
|
||||
// father must not be female; mother must not be male (unknown is allowed).
|
||||
private static async Task<ParentGenderError?> ValidateParents(ApplicationContext db, Guid? fatherId, Guid? motherId)
|
||||
// internal: auch von PUT /gerbils/{id}/parents genutzt (QOL-Eltern-Editor), damit die
|
||||
// Geschlechts-Regel für Eltern an genau EINER Stelle lebt.
|
||||
internal static async Task<ParentGenderError?> ValidateParents(ApplicationContext db, Guid? fatherId, Guid? motherId)
|
||||
{
|
||||
var father = fatherId is Guid f ? await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == f) : null;
|
||||
var mother = motherId is Guid m ? await db.Gerbils.AsNoTracking().FirstOrDefaultAsync(x => x.Id == m) : null;
|
||||
|
||||
Reference in New Issue
Block a user