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, 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, BadRequest, BadRequest>> (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, IsManual = true }; Apply(l, input); db.Litters.Add(l); await db.SaveChangesAsync(); return TypedResults.Created($"/litters/{l.Id}", ToDto(l)); }); group.MapPut("/{id:guid}", async Task, BadRequest>> (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> (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). // 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 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.Stillborn = i.Stillborn; l.FatherId = i.FatherId; l.MotherId = i.MotherId; l.ExpectedGoHomeDate = i.ExpectedGoHomeDate; l.Notes = i.Notes; // Nur überschreiben, wenn explizit gesetzt — sonst bestehenden Wert / Modell-Default // (true) behalten, damit ein Edit ohne dieses Feld einen versteckten Wurf nicht // versehentlich wieder in die Wurfchronik holt. if (i.ShowInChronicle is bool show) l.ShowInChronicle = show; } 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.Stillborn, l.FatherId, l.MotherId, l.ExpectedGoHomeDate, l.Notes, l.Provenance, l.ShowInChronicle); } /// 400 body for a father×mother gender mismatch; frontend localises by Code. public record ParentGenderError(string Code, Gender FatherGender, Gender MotherGender); }