Files
GerbilManager/GerbilManagerWebAPI/Endpoints/LitterEndpoints.cs
Gulum 438c816820 feat: Fehler-melden + Datenherkunft auf Kontakte & Würfe erweitern
- Kontakt-Detailseite: „Fehler melden"- und „Datenherkunft"-Button.
- Wurf-Ansicht: „Datenherkunft"-Button (Feedback war bereits vorhanden).
- Feedback-Entity um loses, nullable ContactId erweitert (kein FK → übersteht
  Ingest-Wipe); Migration AddFeedbackContactId.
- Contact.Provenance + Litter.Provenance (nullable text); Migration
  AddContactLitterProvenance; im Ingest gemappt und in den DTOs zurückgegeben.
- Import: build_entity_provenance() generalisiert; Kontakte (sourceFiles,
  Züchter/Abnehmer-Hinweise) und Würfe (Wurfchronik vs. Diagramm-rekonstruiert,
  Geschwister-Merge) erhalten Herkunftsdaten in resolved_import.json.
- Frontend: ProvenanceDialog generalisiert (EntityProvenance + entityLabel).

Tests erweitert (Ingest-Round-trip Kontakt/Wurf, contact-scoped Feedback
übersteht Wipe). dotnet(212)/vitest(129)/playwright(36)/tsc/eslint grün.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:02:11 +02:00

107 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using GerbilManagerWebAPI.Common;
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Models;
using Gridify;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
namespace GerbilManagerWebAPI.Endpoints
{
public static class LitterEndpoints
{
public static IEndpointRouteBuilder MapLitterEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/litters").WithTags("Litters");
// GET /litters (Gridify: date range + orderBy=date supported on the DateOnly column)
group.MapGet("/", async ([AsParameters] GridifyParams query, ApplicationContext db) =>
TypedResults.Ok(await db.Litters.AsNoTracking().ToPagedResultAsync(query, ToDto)));
group.MapGet("/{id:guid}", async Task<Results<Ok<LitterDto>, NotFound>> (Guid id, ApplicationContext db) =>
{
var l = await db.Litters.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
return l is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(l));
});
group.MapPost("/", async Task<Results<Created<LitterDto>, BadRequest<ParentGenderError>, BadRequest<string>>> (LitterInput input, ApplicationContext db) =>
{
var err = await ValidateParents(db, input.FatherId, input.MotherId);
if (err is not null) return TypedResults.BadRequest(err);
var mortalityErr = ValidateMortality(input.DeathsWithin8Weeks, input.TotalBorn);
if (mortalityErr is not null) return TypedResults.BadRequest(mortalityErr);
var l = new Litter { Id = Guid.NewGuid(), Name = input.Name, Date = input.Date };
Apply(l, input);
db.Litters.Add(l);
await db.SaveChangesAsync();
return TypedResults.Created($"/litters/{l.Id}", ToDto(l));
});
group.MapPut("/{id:guid}", async Task<Results<NoContent, NotFound, BadRequest<ParentGenderError>, BadRequest<string>>> (Guid id, LitterInput input, ApplicationContext db) =>
{
var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id);
if (l is null) return TypedResults.NotFound();
var err = await ValidateParents(db, input.FatherId, input.MotherId);
if (err is not null) return TypedResults.BadRequest(err);
var mortalityErr = ValidateMortality(input.DeathsWithin8Weeks, input.TotalBorn);
if (mortalityErr is not null) return TypedResults.BadRequest(mortalityErr);
l.Name = input.Name;
l.Date = input.Date;
Apply(l, input);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (Guid id, ApplicationContext db) =>
{
var l = await db.Litters.FirstOrDefaultAsync(x => x.Id == id);
if (l is null) return TypedResults.NotFound();
db.Litters.Remove(l);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
return app;
}
// 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)
{
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;
if (father?.Gender == Gender.female || mother?.Gender == Gender.male)
{
return new ParentGenderError("InvalidParentGender",
father?.Gender ?? Gender.unknown, mother?.Gender ?? Gender.unknown);
}
return null;
}
private static void Apply(Litter l, LitterInput i)
{
l.TotalBorn = i.TotalBorn;
l.DeathsWithin8Weeks = i.DeathsWithin8Weeks;
l.FatherId = i.FatherId;
l.MotherId = i.MotherId;
l.ExpectedGoHomeDate = i.ExpectedGoHomeDate;
l.Notes = i.Notes;
}
private static string? ValidateMortality(int? deaths, int? totalBorn)
{
if (deaths is null) return null;
if (deaths < 0) return "DeathsWithin8Weeks darf nicht negativ sein.";
if (totalBorn is not null && deaths > totalBorn)
return "DeathsWithin8Weeks darf nicht größer als TotalBorn sein.";
return null;
}
private static LitterDto ToDto(Litter l) => new(
l.Id, l.Name, l.Date, l.TotalBorn, l.DeathsWithin8Weeks,
l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes, l.Provenance);
}
/// <summary>400 body for a father×mother gender mismatch; frontend localises by Code.</summary>
public record ParentGenderError(string Code, Gender FatherGender, Gender MotherGender);
}