feat(tickets): Rückfrage am Ticket + Antwort durch die Züchterin
Tickets können jetzt eine Rückfrage (Question) tragen und die Züchterin kann
in-app antworten. Feedback um FK-freie Felder Question/Answer/AnsweredAt erweitert
(überlebt Ingest-Wipe); Status-Lebenszyklus Open → NeedsInfo (Rückfrage gestellt)
→ Answered (beantwortet) → Resolved. Migration AddFeedbackQuestionAnswer.
PUT /feedback/{id}: question → NeedsInfo, answer → Answered+AnsweredAt; DTO gibt
die Felder zurück. Tickets-Seite (/hilfe/tickets) zeigt die Rückfrage hervorgehoben
und bietet ein Antwort-Feld + „Antworten"; Status-Badges Offen/Rückfrage offen/
Beantwortet/Gelöst.
Tests: FeedbackEndpointTests (Frage→NeedsInfo, Antwort→Answered, übersteht Ingest),
e2e tickets.spec.ts. dotnet/vitest/playwright grün.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -409,6 +409,9 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
createdAt: new Date().toISOString(),
|
||||
status: 'Open',
|
||||
resolvedAt: null,
|
||||
question: null,
|
||||
answer: null,
|
||||
answeredAt: null,
|
||||
}
|
||||
db.feedback.push(created)
|
||||
return json(route, 201, created)
|
||||
@@ -425,15 +428,47 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
const idx = db.feedback.findIndex((f) => f.id === fid)
|
||||
if (idx < 0) return json(route, 404, { title: 'Not Found' })
|
||||
if (method === 'PUT') {
|
||||
const body = request.postDataJSON() as { message?: string; status?: string }
|
||||
const body = request.postDataJSON() as {
|
||||
message?: string
|
||||
status?: string
|
||||
question?: string
|
||||
answer?: string
|
||||
}
|
||||
const row = db.feedback[idx]
|
||||
if (typeof body.message === 'string') {
|
||||
if (!body.message.trim()) return json(route, 400, 'Message darf nicht leer sein.')
|
||||
row.message = body.message.trim()
|
||||
}
|
||||
// Rückfrage anhängen → NeedsInfo (außer bereits Resolved).
|
||||
if (typeof body.question === 'string') {
|
||||
const q = body.question.trim()
|
||||
row.question = q.length === 0 ? null : q
|
||||
if (row.question && row.status !== 'Resolved') {
|
||||
row.status = 'NeedsInfo'
|
||||
row.resolvedAt = null
|
||||
}
|
||||
}
|
||||
// Antwort der Züchterin → Answered + answeredAt.
|
||||
if (typeof body.answer === 'string') {
|
||||
const a = body.answer.trim()
|
||||
if (a.length === 0) {
|
||||
row.answer = null
|
||||
row.answeredAt = null
|
||||
} else {
|
||||
row.answer = a
|
||||
row.answeredAt = new Date().toISOString()
|
||||
row.status = 'Answered'
|
||||
row.resolvedAt = null
|
||||
}
|
||||
}
|
||||
if (typeof body.status === 'string') {
|
||||
const resolved = body.status.toLowerCase() === 'resolved'
|
||||
row.status = resolved ? 'Resolved' : 'Open'
|
||||
const s = body.status
|
||||
const resolved = s.toLowerCase() === 'resolved'
|
||||
row.status = resolved
|
||||
? 'Resolved'
|
||||
: s === 'NeedsInfo' || s === 'Answered'
|
||||
? s
|
||||
: 'Open'
|
||||
row.resolvedAt = resolved ? (row.resolvedAt ?? new Date().toISOString()) : null
|
||||
}
|
||||
return json(route, 200, row)
|
||||
|
||||
@@ -408,6 +408,28 @@ export function seedDb(): MockDb {
|
||||
createdAt: '2026-06-10T09:00:00Z',
|
||||
status: 'Resolved',
|
||||
resolvedAt: '2026-06-12T14:00:00Z',
|
||||
question: null,
|
||||
answer: null,
|
||||
answeredAt: null,
|
||||
},
|
||||
{
|
||||
// Ticket mit offener Rückfrage (NeedsInfo): die Züchterin soll hier antworten können.
|
||||
id: 'feedback-needsinfo',
|
||||
message: 'Der Wurf B hat ein falsches Geburtsdatum.',
|
||||
context: 'litter-detail',
|
||||
gerbilId: null,
|
||||
litterId: 'wurf-b',
|
||||
contactId: null,
|
||||
entityName: 'Wurf B',
|
||||
url: 'http://localhost:5173/wuerfe/wurf-b',
|
||||
clientTimestamp: '2026-06-14T08:00:00Z',
|
||||
userAgent: null,
|
||||
createdAt: '2026-06-14T08:00:00Z',
|
||||
status: 'NeedsInfo',
|
||||
resolvedAt: null,
|
||||
question: 'Welches Geburtsdatum ist korrekt — der 1. oder der 2. Juni?',
|
||||
answer: null,
|
||||
answeredAt: null,
|
||||
},
|
||||
{
|
||||
id: 'feedback-open',
|
||||
@@ -423,6 +445,9 @@ export function seedDb(): MockDb {
|
||||
createdAt: '2026-06-15T11:30:00Z',
|
||||
status: 'Open',
|
||||
resolvedAt: null,
|
||||
question: null,
|
||||
answer: null,
|
||||
answeredAt: null,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -57,6 +57,28 @@ test.describe('Meine Tickets', () => {
|
||||
await expect(page.getByText('Korrigierte Beschreibung des Fehlers.')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Rückfrage beantworten: Züchterin sieht Frage, antwortet, Badge wird „Beantwortet"', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto('/hilfe/tickets')
|
||||
|
||||
const card = page.locator('.ticket-card').filter({ hasText: 'Wurf B' })
|
||||
await expect(card).toBeVisible()
|
||||
|
||||
// Rückfrage ist sichtbar; Status-Badge „Rückfrage offen".
|
||||
await expect(card.getByText('Welches Geburtsdatum ist korrekt')).toBeVisible()
|
||||
await expect(card.getByText(tt.statusNeedsInfo, { exact: true })).toBeVisible()
|
||||
|
||||
// Antworten.
|
||||
await card.locator('textarea').fill('Korrekt ist der 2. Juni.')
|
||||
await card.getByRole('button', { name: tt.answerButton }).click()
|
||||
|
||||
// Antwort wird angezeigt + Badge wechselt auf „Beantwortet".
|
||||
await expect(card.getByText('Korrekt ist der 2. Juni.')).toBeVisible()
|
||||
await expect(card.locator('.ticket-badge--answered')).toBeVisible()
|
||||
await expect(card.getByText(tt.statusAnswered, { exact: true })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Ticket löschen', async ({ page }) => {
|
||||
await page.goto('/hilfe/tickets')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user