Merge branch 'worktree-agent-a02f88e8490625846'

# Conflicts:
#	GerbilManagerWebAPI/ApplicationContext.cs
#	GerbilManagerWebAPI/Program.cs
#	gerbil-manager-web/e2e/mock-data.ts
#	gerbil-manager-web/src/App.tsx
#	gerbil-manager-web/src/components/AppShell.tsx
#	gerbil-manager-web/src/strings/de.ts
This commit is contained in:
2026-06-22 23:07:22 +02:00
17 changed files with 2646 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
using GerbilManagerWebAPI.Dtos;
using GerbilManagerWebAPI.Models;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
namespace GerbilManagerWebAPI.Endpoints
{
/// <summary>
/// RÜCKNAHMEN (RPRO3 getback_tb): zurückgekommene/zurückgenommene Tiere.
/// GET /returns[?gerbilId=] -> Liste, neueste zuerst (optional nach Tier gefiltert).
/// POST /returns -> Rücknahme erfassen, 201.
/// PUT /returns/{id} -> Rücknahme aktualisieren, 200.
/// DELETE /returns/{id} -> Rücknahme löschen, 204.
/// Entkoppelt von Gerbils/Contacts (lose nullable Guid-Spalten, kein FK), daher
/// überleben Zeilen den Import-Re-Ingest-Wipe — wie Feedback.
/// </summary>
public static class ReturnRecordEndpoints
{
public static IEndpointRouteBuilder MapReturnRecordEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/returns").WithTags("Returns");
group.MapGet("/", async (ApplicationContext db, Guid? gerbilId) =>
{
var query = db.ReturnRecords.AsNoTracking();
if (gerbilId is { } gid)
query = query.Where(r => r.GerbilId == gid);
// In-Memory sortieren: SQLite (Test-Host) kann nicht nach DateTimeOffset ORDER BY-en.
var rows = await query.ToListAsync();
return TypedResults.Ok(rows
.OrderByDescending(r => r.ReturnDate ?? DateTime.MinValue)
.ThenByDescending(r => r.CreatedAt)
.Select(ToDto)
.ToList());
});
group.MapPost("/", async Task<Results<Created<ReturnRecordDto>, BadRequest<string>>> (
ReturnRecordInput input, ApplicationContext db) =>
{
if (input.GerbilId is null && string.IsNullOrWhiteSpace(input.GerbilName))
return TypedResults.BadRequest("Es muss ein Tier (GerbilId oder GerbilName) angegeben werden.");
var entity = new ReturnRecord
{
Id = Guid.NewGuid(),
GerbilId = input.GerbilId,
GerbilName = Trimmed(input.GerbilName),
ReturnDate = input.ReturnDate,
ReturnPrice = input.ReturnPrice,
OriginalPrice = input.OriginalPrice,
OriginalSaleDate = input.OriginalSaleDate,
FromContactId = input.FromContactId,
FromContactName = Trimmed(input.FromContactName),
Note = Trimmed(input.Note),
CreatedAt = DateTimeOffset.UtcNow,
};
db.ReturnRecords.Add(entity);
await db.SaveChangesAsync();
return TypedResults.Created($"/returns/{entity.Id}", ToDto(entity));
});
group.MapPut("/{id:guid}", async Task<Results<Ok<ReturnRecordDto>, NotFound, BadRequest<string>>> (
Guid id, ReturnRecordInput input, ApplicationContext db) =>
{
var entity = await db.ReturnRecords.FirstOrDefaultAsync(r => r.Id == id);
if (entity is null) return TypedResults.NotFound();
if (input.GerbilId is null && string.IsNullOrWhiteSpace(input.GerbilName))
return TypedResults.BadRequest("Es muss ein Tier (GerbilId oder GerbilName) angegeben werden.");
entity.GerbilId = input.GerbilId;
entity.GerbilName = Trimmed(input.GerbilName);
entity.ReturnDate = input.ReturnDate;
entity.ReturnPrice = input.ReturnPrice;
entity.OriginalPrice = input.OriginalPrice;
entity.OriginalSaleDate = input.OriginalSaleDate;
entity.FromContactId = input.FromContactId;
entity.FromContactName = Trimmed(input.FromContactName);
entity.Note = Trimmed(input.Note);
await db.SaveChangesAsync();
return TypedResults.Ok(ToDto(entity));
});
group.MapDelete("/{id:guid}", async Task<Results<NoContent, NotFound>> (
Guid id, ApplicationContext db) =>
{
var entity = await db.ReturnRecords.FirstOrDefaultAsync(r => r.Id == id);
if (entity is null) return TypedResults.NotFound();
db.ReturnRecords.Remove(entity);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
return app;
}
private static string? Trimmed(string? s) =>
string.IsNullOrWhiteSpace(s) ? null : s.Trim();
private static ReturnRecordDto ToDto(ReturnRecord r) =>
new(r.Id, r.GerbilId, r.GerbilName, r.ReturnDate, r.ReturnPrice, r.OriginalPrice,
r.OriginalSaleDate, r.FromContactId, r.FromContactName, r.Note, r.CreatedAt);
}
}