feat(tickets): Wiederöffnen→Rückfrage mit Zeitstempel + Soft-Delete-Papierkorb + Scroll-Erhalt
Wiederöffnen eines gelösten Tickets:
- landet jetzt in „Rückfragen" (NeedsInfo) statt „Offen".
- War es ein echt gelöstes Ticket OHNE Rückfrage, wird um mehr Infos gebeten und
„Wieder geöffnet am" (ReopenedAt) gestempelt.
- Hatte es bereits eine offene Rückfrage (z. B. manuell gelöste Rückfrage), kehrt es
ohne neuen Hinweis und ohne Zeitstempel in genau diese Frage zurück.
Löschen über die UI = Soft-Delete:
- Tickets wandern in die neue Kategorie „Gelöscht" (Papierkorb) statt entfernt zu werden.
- Von dort wiederherstellbar (POST /feedback/{id}/restore → DeletedAt = null).
- Countdown „Wird in N Tagen endgültig gelöscht"; nach 30 Tagen endgültige Löschung
(lazy beim Abruf in GET /feedback).
Außerdem: Scrollposition bleibt beim Zurück-Navigieren (z. B. nach Klick auf einen
Tier-Link) erhalten, statt an den Seitenanfang zu springen.
EF-Migration FeedbackReopenedAndSoftDelete (ReopenedAt, DeletedAt — beide nullable).
Tests: 260 Backend grün (+2 Reopen-Fälle, CRUD auf Soft-Delete umgestellt), e2e Tickets
Desktop+Phone grün (+Soft-Delete/Restore/Reopen), vitest 149.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -431,6 +431,8 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
fixNote: null,
|
||||
agentContext: null,
|
||||
thread: [],
|
||||
reopenedAt: null,
|
||||
deletedAt: null,
|
||||
}
|
||||
db.feedback.push(created)
|
||||
return json(route, 201, created)
|
||||
@@ -440,6 +442,18 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
}
|
||||
return json(route, 405)
|
||||
}
|
||||
// FEEDBACK-TICKETS: Wiederherstellen aus dem Papierkorb (Soft-Delete aufheben).
|
||||
const restoreMatch = path.match(/^\/feedback\/([^/]+)\/restore$/)
|
||||
if (restoreMatch) {
|
||||
const fid = decodeURIComponent(restoreMatch[1])
|
||||
const row = db.feedback.find((f) => f.id === fid)
|
||||
if (!row) return json(route, 404, { title: 'Not Found' })
|
||||
if (method === 'POST') {
|
||||
row.deletedAt = null
|
||||
return json(route, 200, row)
|
||||
}
|
||||
return json(route, 405)
|
||||
}
|
||||
// FEEDBACK-TICKETS: "Meine Tickets" — PUT (Nachricht/Status) und DELETE.
|
||||
const fm = path.match(/^\/feedback\/([^/]+)$/)
|
||||
if (fm) {
|
||||
@@ -456,6 +470,9 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
agentContext?: string
|
||||
}
|
||||
const row = db.feedback[idx]
|
||||
// Zustand vor den Mutationen (für die Wiederöffnen-Erkennung).
|
||||
const wasResolved = row.status === 'Resolved'
|
||||
const hadOpenRueckfrage = typeof row.question === 'string' && !!(row.question as string).trim()
|
||||
if (typeof body.message === 'string') {
|
||||
if (!body.message.trim()) return json(route, 400, 'Message darf nicht leer sein.')
|
||||
row.message = body.message.trim()
|
||||
@@ -504,6 +521,10 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
? s
|
||||
: 'Open'
|
||||
row.resolvedAt = resolved ? (row.resolvedAt ?? new Date().toISOString()) : null
|
||||
// „Wieder geöffnet am" nur, wenn ein echt gelöstes Ticket OHNE offene Rückfrage
|
||||
// wieder geöffnet wird (s. Backend).
|
||||
if (!resolved && wasResolved && !hadOpenRueckfrage)
|
||||
row.reopenedAt = new Date().toISOString()
|
||||
}
|
||||
// Changelog (fixNote) — laienverständlich, sichtbar.
|
||||
if (typeof body.fixNote === 'string') {
|
||||
@@ -518,7 +539,9 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
return json(route, 200, row)
|
||||
}
|
||||
if (method === 'DELETE') {
|
||||
db.feedback.splice(idx, 1)
|
||||
// Soft-Delete: in den Papierkorb verschieben (nicht entfernen).
|
||||
const target = db.feedback[idx]
|
||||
if (!target.deletedAt) target.deletedAt = new Date().toISOString()
|
||||
return json(route, 204)
|
||||
}
|
||||
return json(route, 405)
|
||||
|
||||
Reference in New Issue
Block a user