feat(feedback): Fehler-melden-Dialog + ID-kopieren (Stammbaum/Akte/Wurf)
Rechtsklick auf eine Maus im Stammbaum öffnet ein Kontextmenü mit „ID kopieren" (Clipboard + Toast) und „Fehler melden". Zusätzlich „Fehler melden"-Buttons in der Rennmausakte und der Wurf-Ansicht. Der Dialog erfasst eine Beschreibung und sendet sie samt Debug-Kontext (Ansicht, Tier-/Wurf-ID + Name, URL, Zeitstempel, User-Agent) an POST /feedback; gespeichert in einer neuen Feedback-Tabelle. Backend: Feedback-Entity (lose nullable GerbilId/LitterId ohne FK), Endpoints POST/GET /feedback, EF-Migration AddFeedback. Die Tabelle wird vom Import-Ingest NICHT geleert — Feedback überlebt Re-Ingests (Test deckt das ab). Tests: FeedbackEndpointTests (persistiert, 400 bei leer, übersteht Ingest-Wipe); e2e feedback.spec.ts. tsc/eslint/vitest(129)/playwright(6)/dotnet(212) grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
62
GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs
Normal file
62
GerbilManagerWebAPI/Endpoints/FeedbackEndpoints.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using GerbilManagerWebAPI.Dtos;
|
||||
using GerbilManagerWebAPI.Models;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GerbilManagerWebAPI.Endpoints
|
||||
{
|
||||
/// <summary>
|
||||
/// FEEDBACK: the "Fehler melden" report sink.
|
||||
/// POST /feedback -> persist a user bug report (with captured debug context), returns 201.
|
||||
/// GET /feedback -> list reports, newest first (for later review).
|
||||
/// Feedback is decoupled from gerbils/litters (loose nullable Guid columns, no FK), so
|
||||
/// rows survive the import re-ingest wipe.
|
||||
/// </summary>
|
||||
public static class FeedbackEndpoints
|
||||
{
|
||||
public static IEndpointRouteBuilder MapFeedbackEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var group = app.MapGroup("/feedback").WithTags("Feedback");
|
||||
|
||||
group.MapPost("/", async Task<Results<Created<FeedbackDto>, BadRequest<string>>> (
|
||||
FeedbackInput input, ApplicationContext db, HttpContext http) =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.Message))
|
||||
return TypedResults.BadRequest("Message darf nicht leer sein.");
|
||||
|
||||
var entity = new Feedback
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Message = input.Message.Trim(),
|
||||
Context = string.IsNullOrWhiteSpace(input.Context) ? "unknown" : input.Context.Trim(),
|
||||
GerbilId = input.GerbilId,
|
||||
LitterId = input.LitterId,
|
||||
EntityName = input.EntityName,
|
||||
Url = input.Url,
|
||||
ClientTimestamp = input.ClientTimestamp,
|
||||
UserAgent = http.Request.Headers.UserAgent.ToString() is { Length: > 0 } ua ? ua : null,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
db.Feedback.Add(entity);
|
||||
await db.SaveChangesAsync();
|
||||
return TypedResults.Created($"/feedback/{entity.Id}", ToDto(entity));
|
||||
});
|
||||
|
||||
group.MapGet("/", async (ApplicationContext db) =>
|
||||
{
|
||||
// Order in memory: SQLite (test host) cannot ORDER BY a DateTimeOffset column.
|
||||
var rows = await db.Feedback.AsNoTracking().ToListAsync();
|
||||
return TypedResults.Ok(rows
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Select(ToDto)
|
||||
.ToList());
|
||||
});
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
private static FeedbackDto ToDto(Feedback f) =>
|
||||
new(f.Id, f.Message, f.Context, f.GerbilId, f.LitterId, f.EntityName, f.Url,
|
||||
f.ClientTimestamp, f.UserAgent, f.CreatedAt);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user