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:
@@ -433,6 +433,8 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
thread: [],
|
||||
reopenedAt: null,
|
||||
deletedAt: null,
|
||||
category: null,
|
||||
helpful: null,
|
||||
}
|
||||
db.feedback.push(created)
|
||||
return json(route, 201, created)
|
||||
@@ -468,6 +470,8 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
answer?: string
|
||||
fixNote?: string
|
||||
agentContext?: string
|
||||
category?: string
|
||||
helpful?: boolean
|
||||
}
|
||||
const row = db.feedback[idx]
|
||||
// Zustand vor den Mutationen (für die Wiederöffnen-Erkennung).
|
||||
@@ -536,6 +540,11 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
const c = body.agentContext.trim()
|
||||
row.agentContext = c.length === 0 ? null : c
|
||||
}
|
||||
if (typeof body.category === 'string') {
|
||||
const cat = body.category.trim()
|
||||
row.category = cat.length === 0 ? null : cat
|
||||
}
|
||||
if (typeof body.helpful === 'boolean') row.helpful = body.helpful
|
||||
return json(route, 200, row)
|
||||
}
|
||||
if (method === 'DELETE') {
|
||||
|
||||
@@ -442,6 +442,7 @@ export function seedDb(): MockDb {
|
||||
userAgent: null,
|
||||
createdAt: '2026-06-10T09:00:00Z',
|
||||
status: 'Resolved',
|
||||
category: 'Stammbaum',
|
||||
resolvedAt: '2026-06-12T14:00:00Z',
|
||||
question: null,
|
||||
answer: null,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user