feat(tickets): Foto-/Datei-Anhänge an Tickets

- Anhänge im Melde-Fenster (beim Erstellen) und direkt an bestehenden Tickets:
  Vorschaubilder, Öffnen im neuen Tab, Entfernen.
- Bytes liegen in eigener Tabelle (FeedbackAttachment, lose FeedbackId ohne FK →
  übersteht den Ingest-Wipe); GET /feedback liefert nur Metadaten (id/Name/Typ/Größe),
  die Bytes über /feedback/attachments/{id}. Größenlimit 10 MB.
- Endpoints: POST /feedback/{id}/attachments (base64), GET /feedback/attachments/{id}
  (Bytes), DELETE /feedback/attachments/{id}.

Migration FeedbackAttachments. Tests: 262 Backend grün (+Upload/Serve/Delete +Validierung),
e2e Tickets Desktop+Phone grün (+Foto-Upload), vitest 149.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 11:24:02 +02:00
parent 48b138bbfd
commit 750619d3d7
15 changed files with 2423 additions and 8 deletions

View File

@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
namespace GerbilManagerWebAPI.Models
{
/// <summary>
/// An image/file attached to a feedback ticket (z. B. ein Foto vom Tier/Fellschlag/Stammbaum).
/// Like <see cref="Feedback"/> it is decoupled (loose FeedbackId, no FK) so it survives the
/// import re-ingest wipe. The bytes live in the DB (single-user app, gelegentliche Fotos) —
/// die Liste GET /feedback liefert nur Metadaten, die Bytes kommen über einen eigenen Endpoint.
/// </summary>
public class FeedbackAttachment
{
[Key]
public Guid Id { get; set; }
/// <summary>Loose reference (no FK) to the feedback ticket this belongs to.</summary>
public Guid FeedbackId { get; set; }
public required string FileName { get; set; }
public required string ContentType { get; set; }
/// <summary>Größe in Bytes (separat gespeichert, damit Listen-Abfragen die Bytes nicht laden).</summary>
public int Size { get; set; }
/// <summary>The raw file bytes.</summary>
public required byte[] Data { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
}