feat(tickets): Volltextsuche + Kategorien, 👍/👎-Rückmeldung & ähnliche gelöste Tickets

- Suche: Volltext über Nachricht/Frage/Antwort/Changelog/Name/Kategorie, plus
  Kategorie-Filter (KI-gesetzte Kategorie als Chip + Auswahlmenü), je innerhalb der
  aktiven Ansicht.
- 👍/👎 auf gelösten Tickets: 👍 speichert die Rückmeldung, 👎 öffnet das Ticket
  wieder (zurück in die Rückfragen) und bittet um die fehlende Info.
- Melde-Fenster: schlägt beim Tippen ähnliche bereits gelöste Tickets vor
  (Wort-Überlappung + Bonus bei gleichem Tier/Wurf/Kontakt) — Self-Service.

Backend: Feedback.Category + Feedback.Helpful (+ PUT-Handling), Migration
FeedbackCategoryAndHelpful (beide nullable). Tests: 260 Backend grün, e2e Tickets
Desktop +4 (Suche/Kategorie/👍/👎), vitest 149.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 11:14:07 +02:00
parent c0a7b1a5ee
commit 48b138bbfd
15 changed files with 2217 additions and 10 deletions

View File

@@ -248,4 +248,67 @@ test.describe('Meine Tickets', () => {
await expect(reopened.getByText(tt.reopenRequest)).toBeVisible()
await expect(reopened.getByText(tt.reopenedLabel)).toBeVisible()
})
test('Volltextsuche filtert die Ticket-Liste', async ({ page }) => {
await page.goto('/hilfe/tickets')
await selectView(page, tt.filters.closed)
const search = page.getByPlaceholder(tt.searchPlaceholder)
await search.fill('Krümel')
await expect(
page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'),
).toBeVisible()
await search.fill('xyzgibtsnicht')
await expect(page.getByText(tt.emptySearch)).toBeVisible()
})
test('Kategorie-Filter zeigt nur Tickets der gewählten Kategorie', async ({ page }) => {
await page.goto('/hilfe/tickets')
await selectView(page, tt.filters.closed)
// Das Krümel-Ticket hat die Kategorie „Stammbaum" → als Chip sichtbar.
const card = page
.locator('.ticket-card')
.filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' })
await expect(card.locator('.ticket-card__category')).toHaveText('Stammbaum')
// Über das Kategorie-Menü filtern.
await page.getByLabel(tt.categoryFilterLabel).selectOption('Stammbaum')
await expect(
page.getByText('Der Stammbaum zeigt den falschen Vater bei Krümel.'),
).toBeVisible()
})
test('👍 auf gelöstem Ticket speichert Rückmeldung; 👎 öffnet es wieder', async ({ page }) => {
await page.goto('/hilfe/tickets')
await selectView(page, tt.filters.closed)
const card = page
.locator('.ticket-card')
.filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' })
await expect(card.getByText(tt.helpfulQuestion)).toBeVisible()
// 👍 → Dank-Text, kein Wiederöffnen.
await card.getByRole('button', { name: tt.helpfulYes }).click()
await expect(card.getByText(tt.helpfulThanks)).toBeVisible()
})
test('👎 auf gelöstem Ticket öffnet es als Rückfrage wieder', async ({ page }) => {
await page.goto('/hilfe/tickets')
await selectView(page, tt.filters.closed)
const card = page
.locator('.ticket-card')
.filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' })
await card.getByRole('button', { name: tt.helpfulNo }).click()
// Verlässt „Geschlossen", landet in „Rückfragen" mit Bitte um Infos.
await selectView(page, tt.filters.dialog)
const reopened = page
.locator('.ticket-card')
.filter({ hasText: 'Der Stammbaum zeigt den falschen Vater bei Krümel.' })
await expect(reopened.locator('.ticket-badge--needsinfo')).toBeVisible()
await expect(reopened.getByText(tt.helpfulReopenNote)).toBeVisible()
})
})