feat(tickets): Erst-Text nach Antwort sperren, nur neueste Antwort editierbar
Sobald ein Ticket eine Antwort hat, ist der ursprüngliche Ticket-Text nicht mehr bearbeitbar. Editierbar ist dann nur noch die neueste Antwort — und auch die nur, bis die KI darauf reagiert hat (Status verlässt „Answered"). Ältere Antworten im Verlauf (thread) bleiben wie bisher schreibgeschützt. Regressionstest + Answered-Mock-Ticket ergänzt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -480,6 +480,29 @@ export function seedDb(): MockDb {
|
|||||||
{ role: 'breeder', text: 'Im Juni.', at: '2026-06-13T18:00:00Z' },
|
{ role: 'breeder', text: 'Im Juni.', at: '2026-06-13T18:00:00Z' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Beantwortetes Ticket (Answered): die KI hat noch NICHT reagiert. Der Erst-Text ist
|
||||||
|
// jetzt gesperrt (kein „Bearbeiten"), nur die neueste Antwort ist noch änderbar.
|
||||||
|
id: 'feedback-answered',
|
||||||
|
message: 'Bei Karlsson stimmt der Farbschlag nicht.',
|
||||||
|
context: 'gerbil-detail',
|
||||||
|
gerbilId: 'karlsson',
|
||||||
|
litterId: null,
|
||||||
|
contactId: null,
|
||||||
|
entityName: 'Karlsson',
|
||||||
|
url: 'http://localhost:5173/rennmaeuse/karlsson',
|
||||||
|
clientTimestamp: '2026-06-16T09:00:00Z',
|
||||||
|
userAgent: null,
|
||||||
|
createdAt: '2026-06-16T09:00:00Z',
|
||||||
|
status: 'Answered',
|
||||||
|
resolvedAt: null,
|
||||||
|
question: 'Welcher Farbschlag ist korrekt?',
|
||||||
|
answer: 'Karlsson ist Blau, nicht Schwarz.',
|
||||||
|
answeredAt: '2026-06-16T18:00:00Z',
|
||||||
|
fixNote: null,
|
||||||
|
agentContext: null,
|
||||||
|
thread: [],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'feedback-open',
|
id: 'feedback-open',
|
||||||
message: 'Das Gewicht wird auf der Verlaufskurve falsch gerundet.',
|
message: 'Das Gewicht wird auf der Verlaufskurve falsch gerundet.',
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ test.describe('Meine Tickets', () => {
|
|||||||
const open = page.locator('.tickets-filter').filter({ hasText: tt.filters.open })
|
const open = page.locator('.tickets-filter').filter({ hasText: tt.filters.open })
|
||||||
const closed = page.locator('.tickets-filter').filter({ hasText: tt.filters.closed })
|
const closed = page.locator('.tickets-filter').filter({ hasText: tt.filters.closed })
|
||||||
await expect(dialog.locator('.tickets-filter__count')).toHaveText('1')
|
await expect(dialog.locator('.tickets-filter__count')).toHaveText('1')
|
||||||
await expect(open.locator('.tickets-filter__count')).toHaveText('1')
|
// „Offen" = Open + Answered → das offene + das beantwortete Ticket = 2.
|
||||||
|
await expect(open.locator('.tickets-filter__count')).toHaveText('2')
|
||||||
await expect(closed.locator('.tickets-filter__count')).toHaveText('1')
|
await expect(closed.locator('.tickets-filter__count')).toHaveText('1')
|
||||||
|
|
||||||
// „Offen": nur das offene Ticket.
|
// „Offen": nur das offene Ticket.
|
||||||
@@ -131,6 +132,20 @@ test.describe('Meine Tickets', () => {
|
|||||||
await expect(page.getByText('Korrigierte Beschreibung des Fehlers.')).toBeVisible()
|
await expect(page.getByText('Korrigierte Beschreibung des Fehlers.')).toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('beantwortetes Ticket: Erst-Text gesperrt, nur die neueste Antwort ist änderbar', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/hilfe/tickets')
|
||||||
|
// „Offen"-Ansicht (Default) zeigt auch beantwortete Tickets (Answered).
|
||||||
|
const card = page.locator('.ticket-card').filter({ hasText: 'Karlsson ist Blau' })
|
||||||
|
await expect(card).toBeVisible()
|
||||||
|
|
||||||
|
// Erst-Text ist gesperrt: kein „Bearbeiten"-Button mehr, sobald es eine Antwort gibt.
|
||||||
|
await expect(card.getByRole('button', { name: tt.edit })).toHaveCount(0)
|
||||||
|
// Die neueste Antwort ist dagegen noch änderbar (KI hat noch nicht reagiert → Answered).
|
||||||
|
await expect(card.getByRole('button', { name: tt.amendAnswer })).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
test('Rückfrage beantworten: Züchterin sieht Frage, antwortet, Badge wird „Beantwortet"', async ({
|
test('Rückfrage beantworten: Züchterin sieht Frage, antwortet, Badge wird „Beantwortet"', async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -535,9 +535,11 @@ function TicketCard({ ticket, onToggleStatus, onSaveMessage, onAnswer, onDelete
|
|||||||
const canAmend = !!ticket.answer && ticket.status === 'Answered'
|
const canAmend = !!ticket.answer && ticket.status === 'Answered'
|
||||||
// Antwort-Formular zeigen: neue Antwort, aktives Ändern, oder ein gepufferter Änderungs-Entwurf.
|
// Antwort-Formular zeigen: neue Antwort, aktives Ändern, oder ein gepufferter Änderungs-Entwurf.
|
||||||
const showAnswerForm = canAnswerNew || answerEditing || (canAmend && !!answerDraft)
|
const showAnswerForm = canAnswerNew || answerEditing || (canAmend && !!answerDraft)
|
||||||
// Bearbeiten (Beschreibung) ist gesperrt, solange eine Rückfrage offen ist (NeedsInfo):
|
// Der ursprüngliche Ticket-Text ist nur bearbeitbar, SOLANGE es noch keine Antwort gibt
|
||||||
// dann soll die Züchterin ANTWORTEN, nicht den Text ändern.
|
// UND keine Rückfrage offen ist (NeedsInfo). Sobald die Züchterin geantwortet hat (oder die
|
||||||
const canEditMessage = ticket.status !== 'NeedsInfo'
|
// KI nachgefragt hat), bleibt der Erst-Text fix — editierbar ist dann nur noch die neueste
|
||||||
|
// Antwort (und auch die nur, bis die KI darauf reagiert hat, s. canAmend).
|
||||||
|
const canEditMessage = !ticket.answer && ticket.status !== 'NeedsInfo'
|
||||||
|
|
||||||
function startAmend() {
|
function startAmend() {
|
||||||
if (!answerDraft) setAnswerDraft(ticket.answer ?? '')
|
if (!answerDraft) setAnswerDraft(ticket.answer ?? '')
|
||||||
|
|||||||
Reference in New Issue
Block a user