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:
@@ -435,15 +435,51 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
deletedAt: null,
|
||||
category: null,
|
||||
helpful: null,
|
||||
attachments: [],
|
||||
}
|
||||
db.feedback.push(created)
|
||||
return json(route, 201, created)
|
||||
}
|
||||
if (method === 'GET') {
|
||||
return json(route, 200, [...db.feedback].reverse())
|
||||
// attachments immer als Array liefern (Mock-Daten haben das Feld evtl. nicht).
|
||||
const rows = [...db.feedback].reverse().map((f) => ({ ...f, attachments: f.attachments ?? [] }))
|
||||
return json(route, 200, rows)
|
||||
}
|
||||
return json(route, 405)
|
||||
}
|
||||
// FEEDBACK-ANHÄNGE: hochladen (POST /feedback/{id}/attachments).
|
||||
const attUpload = path.match(/^\/feedback\/([^/]+)\/attachments$/)
|
||||
if (attUpload && method === 'POST') {
|
||||
const fid = decodeURIComponent(attUpload[1])
|
||||
const row = db.feedback.find((f) => f.id === fid)
|
||||
if (!row) return json(route, 404, { title: 'Not Found' })
|
||||
const body = request.postDataJSON() as { fileName?: string; contentType?: string; dataBase64?: string }
|
||||
const meta = {
|
||||
id: newId('att'),
|
||||
fileName: body.fileName ?? 'datei',
|
||||
contentType: body.contentType ?? 'application/octet-stream',
|
||||
size: (body.dataBase64 ?? '').length,
|
||||
}
|
||||
const list = (row.attachments as unknown[] | undefined) ?? []
|
||||
list.push(meta)
|
||||
row.attachments = list
|
||||
return json(route, 201, meta)
|
||||
}
|
||||
// FEEDBACK-ANHÄNGE: löschen (DELETE /feedback/attachments/{attId}).
|
||||
const attDelete = path.match(/^\/feedback\/attachments\/([^/]+)$/)
|
||||
if (attDelete && method === 'DELETE') {
|
||||
const attId = decodeURIComponent(attDelete[1])
|
||||
for (const f of db.feedback) {
|
||||
const list = (f.attachments as { id: string }[] | undefined) ?? []
|
||||
const i = list.findIndex((a) => a.id === attId)
|
||||
if (i >= 0) {
|
||||
list.splice(i, 1)
|
||||
f.attachments = list
|
||||
return json(route, 204)
|
||||
}
|
||||
}
|
||||
return json(route, 404, { title: 'Not Found' })
|
||||
}
|
||||
// FEEDBACK-TICKETS: Wiederherstellen aus dem Papierkorb (Soft-Delete aufheben).
|
||||
const restoreMatch = path.match(/^\/feedback\/([^/]+)\/restore$/)
|
||||
if (restoreMatch) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user