feat(warteliste): Nachfrage/Warteliste für Interessenten
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
122
GerbilManagerWebAPI/Endpoints/WaitingListEndpoints.cs
Normal file
122
GerbilManagerWebAPI/Endpoints/WaitingListEndpoints.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using GerbilManagerWebAPI.Dtos;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GerbilManagerWebAPI.Endpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// WAITLIST (RennmausPro nachfrage_tb): a standing list of interested parties waiting
|
||||
/// for a future animal matching their wish criteria (colour, gender).
|
||||
/// GET /waiting-list -> list entries, newest request first
|
||||
/// POST /waiting-list -> create an entry (returns 201)
|
||||
/// PUT /waiting-list/{id} -> update / change status
|
||||
/// DELETE /waiting-list/{id} -> remove an entry
|
||||
/// Decoupled from contacts (loose nullable ContactId, no FK), so rows survive the
|
||||
/// import re-ingest wipe — exactly like Feedback.
|
||||
/// </summary>
|
||||
public static class WaitingListEndpoints
|
||||
{
|
||||
/// <summary>Allowed workflow statuses (frontend contract).</summary>
|
||||
private static readonly string[] AllowedStatuses = { "offen", "erfuellt", "storniert" };
|
||||
private const string DefaultStatus = "offen";
|
||||
|
||||
public static IEndpointRouteBuilder MapWaitingListEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/waiting-list").WithTags("WaitingList");
|
||||
|
||||
group.MapGet("/", async (ApplicationContext db) =>
|
||||
{
|
||||
// Order in memory: SQLite (test host) cannot ORDER BY a DateTimeOffset column,
|
||||
// and RequestedAt is nullable — sort entries with a date first, newest first.
|
||||
var rows = await db.WaitingListEntries.AsNoTracking().ToListAsync();
|
||||
return TypedResults.Ok(rows
|
||||
.OrderByDescending(e => e.RequestedAt ?? DateTime.MinValue)
|
||||
.ThenByDescending(e => e.CreatedAt)
|
||||
.Select(ToDto)
|
||||
.ToList());
|
||||
});
|
||||
|
||||
group.MapGet("/{id:guid}", async Task<Results<Ok<WaitingListDto>, NotFound>> (
|
||||
Guid id, ApplicationContext db) =>
|
||||
{
|
||||
var entity = await db.WaitingListEntries.AsNoTracking().FirstOrDefaultAsync(e => e.Id == id);
|
||||
return entity is null ? TypedResults.NotFound() : TypedResults.Ok(ToDto(entity));
|
||||
});
|
||||
|
||||
group.MapPost("/", async Task<Results<Created<WaitingListDto>, BadRequest<string>>> (
|
||||
WaitingListInput input, ApplicationContext db) =>
|
||||
{
|
||||
var status = NormalizeStatus(input.Status);
|
||||
if (status is null)
|
||||
return TypedResults.BadRequest("Ungültiger Status.");
|
||||
if (string.IsNullOrWhiteSpace(input.ContactName) && input.ContactId is null)
|
||||
return TypedResults.BadRequest("Kontakt oder Name ist erforderlich.");
|
||||
|
||||
var entity = new WaitingListEntry
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
ContactId = input.ContactId,
|
||||
ContactName = Trim(input.ContactName),
|
||||
WishColor = Trim(input.WishColor),
|
||||
WishGender = Trim(input.WishGender),
|
||||
RequestedAt = input.RequestedAt,
|
||||
Status = status,
|
||||
Note = Trim(input.Note),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
db.WaitingListEntries.Add(entity);
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.Created($"/waiting-list/{entity.Id}", ToDto(entity));
|
||||
});
|
||||
|
||||
group.MapPut("/{id:guid}", async Task<Results<Ok<WaitingListDto>, NotFound, BadRequest<string>>> (
|
||||
Guid id, WaitingListInput input, ApplicationContext db) =>
|
||||
{
|
||||
var entity = await db.WaitingListEntries.FirstOrDefaultAsync(e => e.Id == id);
|
||||
if (entity is null) return TypedResults.NotFound();
|
||||
|
||||
var status = NormalizeStatus(input.Status);
|
||||
if (status is null)
|
||||
return TypedResults.BadRequest("Ungültiger Status.");
|
||||
if (string.IsNullOrWhiteSpace(input.ContactName) && input.ContactId is null)
|
||||
return TypedResults.BadRequest("Kontakt oder Name ist erforderlich.");
|
||||
|
||||
entity.ContactId = input.ContactId;
|
||||
entity.ContactName = Trim(input.ContactName);
|
||||
entity.WishColor = Trim(input.WishColor);
|
||||
entity.WishGender = Trim(input.WishGender);
|
||||
entity.RequestedAt = input.RequestedAt;
|
||||
entity.Status = status;
|
||||
entity.Note = Trim(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.WaitingListEntries.FirstOrDefaultAsync(e => e.Id == id);
|
||||
if (entity is null) return TypedResults.NotFound();
|
||||
db.WaitingListEntries.Remove(entity);
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.NoContent();
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
/// <summary>Empty/whitespace status defaults to "offen"; unknown values are rejected (null).</summary>
|
||||
private static string? NormalizeStatus(string? status)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(status)) return DefaultStatus;
|
||||
var trimmed = status.Trim();
|
||||
return AllowedStatuses.Contains(trimmed) ? trimmed : null;
|
||||
}
|
||||
|
||||
private static string? Trim(string? s) => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
|
||||
|
||||
private static WaitingListDto ToDto(WaitingListEntry e) =>
|
||||
new(e.Id, e.ContactId, e.ContactName, e.WishColor, e.WishGender, e.RequestedAt, e.Status, e.Note, e.CreatedAt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user