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

@@ -311,4 +311,25 @@ test.describe('Meine Tickets', () => {
await expect(reopened.locator('.ticket-badge--needsinfo')).toBeVisible()
await expect(reopened.getByText(tt.helpfulReopenNote)).toBeVisible()
})
test('Foto an ein Ticket anhängen erscheint als Vorschaubild', async ({ page }) => {
await page.goto('/hilfe/tickets')
// Offenes Ticket (Fridolin) — Anhang-Bereich mit „Foto anhängen".
const card = page.locator('.ticket-card').filter({ hasText: 'Fridolin' })
await expect(card.getByText(tt.attachmentsLabel, { exact: true })).toBeVisible()
// 1×1-PNG als Datei hochladen.
const png = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
)
await card.locator('input[type="file"]').setInputFiles({
name: 'maus.png',
mimeType: 'image/png',
buffer: png,
})
// Nach dem Upload erscheint ein Vorschaubild im Anhang-Raster.
await expect(card.locator('.ticket-card__attachment img')).toHaveCount(1)
})
})