feat(ausstellungen): Ausstellungs-/Auszeichnungsergebnisse je Tier

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:43:19 +02:00
parent 5e14124322
commit d380a0c2ef
16 changed files with 2520 additions and 2 deletions

View File

@@ -0,0 +1,49 @@
/** EXHIBITION: Ausstellungs-/Show-Ergebnisse je Tier — Tab in der Rennmausakte. */
import { de, expect, skipUnlessMock, test } from './fixtures'
const t = de.pages.tierTabs.exhibitions
const ta = de.pages.tierTabs.actions
const tabs = de.pages.gerbils.detail.tabs
test('Rennmausakte: Ausstellungsergebnis anlegen, sehen und löschen', async ({ page, mockDb }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel')
// Auf den "Ausstellungen"-Tab wechseln.
await page.getByRole('tab', { name: tabs.exhibitions }).click()
// Leerzustand sichtbar.
await expect(page.getByText(t.empty)).toBeVisible()
// Neues Ergebnis erfassen.
await page.getByRole('button', { name: new RegExp(t.newButton) }).click()
await page.getByLabel(new RegExp(t.fields.eventName)).fill('Nationale Rennmausschau 2026')
await page.getByLabel(t.fields.placement).fill('1. Platz')
await page.getByLabel(t.fields.award).fill('Best in Show')
await page.getByRole('button', { name: ta.save, exact: true }).click()
// Ergebnis erscheint in der Liste.
await expect(page.getByText('Nationale Rennmausschau 2026')).toBeVisible()
await expect(page.getByText('1. Platz')).toBeVisible()
// Wurde im Mock mit Tier-Bezug gespeichert.
expect(mockDb).not.toBeNull()
const row = mockDb!.exhibitions.find((r) => r.eventName === 'Nationale Rennmausschau 2026')
expect(row).toMatchObject({ gerbilId: 'kruemel', placement: '1. Platz', award: 'Best in Show' })
// Löschen (window.confirm automatisch annehmen).
page.once('dialog', (d) => void d.accept())
await page.getByRole('button', { name: ta.delete }).click()
await expect(page.getByText(t.empty)).toBeVisible()
})
test('Rennmausakte: leere Veranstaltung wird abgelehnt (Validierung)', async ({ page }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/kruemel')
await page.getByRole('tab', { name: tabs.exhibitions }).click()
await page.getByRole('button', { name: new RegExp(t.newButton) }).click()
// Ohne Veranstaltung speichern → Validierungsfehler, kein Eintrag.
await page.getByRole('button', { name: ta.save, exact: true }).click()
await expect(page.getByText(t.validation.eventNameRequired)).toBeVisible()
})

View File

@@ -416,6 +416,59 @@ export async function installMockApi(page: Page): Promise<MockDb> {
return json(route, 405)
}
// EXHIBITION: Ausstellungs-/Show-Ergebnisse je Tier (POST/GET ?gerbilId=, PUT, DELETE).
// Plain-Liste (kein Gridify), gefiltert per ?gerbilId, neueste zuerst.
if (path === '/exhibitions') {
if (method === 'GET') {
const gerbilId = url.searchParams.get('gerbilId')
const rows = (db.exhibitions as Row[]).filter(
(r) => !gerbilId || r.gerbilId === gerbilId,
)
const sorted = [...rows].sort((a, b) =>
String(b.date ?? '').localeCompare(String(a.date ?? '')),
)
return json(route, 200, sorted)
}
if (method === 'POST') {
const body = request.postDataJSON() as Row
if (!String(body.eventName ?? '').trim())
return json(route, 400, 'EventName darf nicht leer sein.')
const created = {
id: newId('exhibition'),
gerbilId: body.gerbilId ?? null,
entityName: body.entityName ?? null,
eventName: String(body.eventName).trim(),
date: body.date ?? null,
placement: body.placement ?? null,
award: body.award ?? null,
note: body.note ?? null,
createdAt: new Date().toISOString(),
}
;(db.exhibitions as Row[]).push(created)
return json(route, 201, created)
}
return json(route, 405)
}
const em = path.match(/^\/exhibitions\/([^/]+)$/)
if (em) {
const eid = decodeURIComponent(em[1])
const idx = (db.exhibitions as Row[]).findIndex((r) => r.id === eid)
if (idx < 0) return json(route, 404, { title: 'Not Found' })
if (method === 'GET') return json(route, 200, db.exhibitions[idx])
if (method === 'PUT') {
const body = request.postDataJSON() as Row
if ('eventName' in body && !String(body.eventName ?? '').trim())
return json(route, 400, 'EventName darf nicht leer sein.')
Object.assign(db.exhibitions[idx], body)
return json(route, 200, db.exhibitions[idx])
}
if (method === 'DELETE') {
;(db.exhibitions as Row[]).splice(idx, 1)
return json(route, 204)
}
return json(route, 405)
}
// Generische Kollektionen: /<resource> und /<resource>/<id>
m = path.match(/^\/([a-z-]+)(?:\/([^/]+))?$/)
const col = m ? collections[m[1]] : undefined

View File

@@ -78,6 +78,8 @@ export interface MockDb {
namesConfigured: boolean
// FEEDBACK: "Fehler melden" — gesammelte Berichte (POST /feedback)
feedback: Record<string, unknown>[]
// EXHIBITION: Ausstellungs-/Show-Ergebnisse je Tier (POST /exhibitions)
exhibitions: Record<string, unknown>[]
}
function gerbil(
@@ -376,5 +378,6 @@ export function seedDb(): MockDb {
saleAdConfigured: true,
namesConfigured: true,
feedback: [],
exhibitions: [],
}
}