- 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>
64 lines
2.7 KiB
C#
64 lines
2.7 KiB
C#
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,
|
|
ContactId = input.ContactId,
|
|
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.ContactId, f.EntityName, f.Url,
|
|
f.ClientTimestamp, f.UserAgent, f.CreatedAt);
|
|
}
|
|
}
|