feat(warteliste): Nachfrage/Warteliste für Interessenten
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -416,6 +416,73 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
return json(route, 405)
|
||||
}
|
||||
|
||||
// WAITLIST: Warteliste/Nachfrage — GET liefert ein BLANKES Array (kein Gridify-Envelope),
|
||||
// POST/PUT/DELETE wie eine einfache Kollektion. Vor den generischen Kollektionen, weil
|
||||
// die GET-Antwort kein paginiertes Envelope ist.
|
||||
const wlm = path.match(/^\/waiting-list(?:\/([^/]+))?$/)
|
||||
if (wlm) {
|
||||
const wlId = wlm[1] ? decodeURIComponent(wlm[1]) : null
|
||||
const ALLOWED = ['offen', 'erfuellt', 'storniert']
|
||||
const normStatus = (s: unknown): string | null => {
|
||||
if (s === undefined || s === null || String(s).trim() === '') return 'offen'
|
||||
const v = String(s).trim()
|
||||
return ALLOWED.includes(v) ? v : null
|
||||
}
|
||||
if (!wlId) {
|
||||
if (method === 'GET') {
|
||||
// Neueste Anfrage zuerst (requestedAt desc, dann createdAt desc).
|
||||
const sorted = [...db.waitingList].sort((a, b) => {
|
||||
const ar = String(a.requestedAt ?? '')
|
||||
const br = String(b.requestedAt ?? '')
|
||||
if (ar !== br) return ar < br ? 1 : -1
|
||||
return String(a.createdAt ?? '') < String(b.createdAt ?? '') ? 1 : -1
|
||||
})
|
||||
return json(route, 200, sorted)
|
||||
}
|
||||
if (method === 'POST') {
|
||||
const body = request.postDataJSON() as Record<string, unknown>
|
||||
const status = normStatus(body.status)
|
||||
if (status === null) return json(route, 400, 'Ungültiger Status.')
|
||||
if (!body.contactId && (!body.contactName || String(body.contactName).trim() === ''))
|
||||
return json(route, 400, 'Kontakt oder Name ist erforderlich.')
|
||||
const created = {
|
||||
id: newId('wl'),
|
||||
contactId: body.contactId ?? null,
|
||||
contactName: body.contactName ?? null,
|
||||
wishColor: body.wishColor ?? null,
|
||||
wishGender: body.wishGender ?? null,
|
||||
requestedAt: body.requestedAt ?? null,
|
||||
status,
|
||||
note: body.note ?? null,
|
||||
createdAt: new Date().toISOString(),
|
||||
}
|
||||
db.waitingList.push(created)
|
||||
return json(route, 201, created)
|
||||
}
|
||||
return json(route, 405)
|
||||
}
|
||||
const wlIdx = db.waitingList.findIndex((r) => r.id === wlId)
|
||||
if (method === 'GET') {
|
||||
return wlIdx >= 0 ? json(route, 200, db.waitingList[wlIdx]) : json(route, 404, { title: 'Not Found' })
|
||||
}
|
||||
if (method === 'PUT') {
|
||||
if (wlIdx < 0) return json(route, 404, { title: 'Not Found' })
|
||||
const body = request.postDataJSON() as Record<string, unknown>
|
||||
const status = normStatus(body.status)
|
||||
if (status === null) return json(route, 400, 'Ungültiger Status.')
|
||||
if (!body.contactId && (!body.contactName || String(body.contactName).trim() === ''))
|
||||
return json(route, 400, 'Kontakt oder Name ist erforderlich.')
|
||||
Object.assign(db.waitingList[wlIdx], { ...body, status })
|
||||
return json(route, 200, db.waitingList[wlIdx])
|
||||
}
|
||||
if (method === 'DELETE') {
|
||||
if (wlIdx < 0) return json(route, 404, { title: 'Not Found' })
|
||||
db.waitingList.splice(wlIdx, 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
|
||||
|
||||
@@ -78,6 +78,8 @@ export interface MockDb {
|
||||
namesConfigured: boolean
|
||||
// FEEDBACK: "Fehler melden" — gesammelte Berichte (POST /feedback)
|
||||
feedback: Record<string, unknown>[]
|
||||
// WAITLIST: Warteliste/Nachfrage (RennmausPro nachfrage_tb)
|
||||
waitingList: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
function gerbil(
|
||||
@@ -376,5 +378,29 @@ export function seedDb(): MockDb {
|
||||
saleAdConfigured: true,
|
||||
namesConfigured: true,
|
||||
feedback: [],
|
||||
waitingList: [
|
||||
{
|
||||
id: 'wl-seed-1',
|
||||
contactId: contacts[0]?.id ?? null,
|
||||
contactName: contacts[0]?.name ?? 'Familie Sonntag',
|
||||
wishColor: 'Schwarz',
|
||||
wishGender: 'female',
|
||||
requestedAt: '2026-05-01T00:00:00Z',
|
||||
status: 'offen',
|
||||
note: 'möchte zwei Weibchen',
|
||||
createdAt: '2026-05-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'wl-seed-2',
|
||||
contactId: null,
|
||||
contactName: 'Herr Maier',
|
||||
wishColor: null,
|
||||
wishGender: 'male',
|
||||
requestedAt: '2026-04-10T00:00:00Z',
|
||||
status: 'erfuellt',
|
||||
note: null,
|
||||
createdAt: '2026-04-10T00:00:00Z',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
73
gerbil-manager-web/e2e/warteliste.spec.ts
Normal file
73
gerbil-manager-web/e2e/warteliste.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/** WAITLIST: Warteliste/Nachfrage — Liste, Anlegen, Status setzen, "nur offene" filtern. */
|
||||
import { acceptNextDialog, de, expect, gotoSection, skipUnlessMock, test } from './fixtures'
|
||||
|
||||
const t = de.pages.warteliste
|
||||
|
||||
test('Warteliste ist über die Navigation erreichbar und zeigt Seed-Einträge', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await gotoSection(page, de.nav.waitingList)
|
||||
await expect(page.getByRole('heading', { name: t.title, exact: true })).toBeVisible()
|
||||
|
||||
// Seed: ein Kontakt-Link + ein freitextlicher Name.
|
||||
await expect(page.getByRole('link', { name: 'Zoohandlung Meier' })).toBeVisible()
|
||||
await expect(page.getByText('Herr Maier')).toBeVisible()
|
||||
})
|
||||
|
||||
test('"Nur offene" filtert erfüllte Einträge aus', async ({ page }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/warteliste')
|
||||
await expect(page.getByText('Herr Maier')).toBeVisible() // erfüllt
|
||||
|
||||
await page.getByLabel(t.onlyOpen).check()
|
||||
await expect(page.getByText('Herr Maier')).toHaveCount(0)
|
||||
await expect(page.getByRole('link', { name: 'Zoohandlung Meier' })).toBeVisible() // offen
|
||||
})
|
||||
|
||||
test('Neuer Eintrag anlegen erscheint in der Liste', async ({ page, mockDb }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/warteliste')
|
||||
|
||||
await page.getByRole('button', { name: `+ ${t.newButton}` }).click()
|
||||
const form = page.getByRole('form', { name: t.formTitleNew })
|
||||
await expect(form).toBeVisible()
|
||||
|
||||
await form.getByLabel(t.fields.contactName).fill('Neuinteressent Test')
|
||||
await form.getByLabel(t.fields.wishColor).selectOption({ label: 'Schwarz' })
|
||||
await form.getByLabel(t.fields.wishGender).selectOption({ label: t.wishGender.male })
|
||||
await form.getByRole('button', { name: t.save }).click()
|
||||
|
||||
await expect(page.getByText(de.common.saved)).toBeVisible()
|
||||
await expect(page.getByText('Neuinteressent Test')).toBeVisible()
|
||||
|
||||
expect(mockDb).not.toBeNull()
|
||||
const created = mockDb!.waitingList.find((e) => e.contactName === 'Neuinteressent Test')
|
||||
expect(created).toBeTruthy()
|
||||
expect(created).toMatchObject({ status: 'offen', wishColor: 'Schwarz', wishGender: 'male' })
|
||||
})
|
||||
|
||||
test('Status je Eintrag direkt umstellbar', async ({ page, mockDb }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/warteliste')
|
||||
|
||||
// Status-Select des offenen Seed-Eintrags (Zoohandlung Meier) auf "erfüllt" stellen.
|
||||
const statusSelect = page.getByLabel(`${t.fields.status} Zoohandlung Meier`)
|
||||
await statusSelect.selectOption({ label: t.status.erfuellt })
|
||||
await expect(page.getByText(de.common.saved)).toBeVisible()
|
||||
|
||||
expect(mockDb!.waitingList.find((e) => e.id === 'wl-seed-1')?.status).toBe('erfuellt')
|
||||
})
|
||||
|
||||
test('Eintrag löschen entfernt ihn aus der Liste', async ({ page, mockDb }) => {
|
||||
skipUnlessMock()
|
||||
await page.goto('/warteliste')
|
||||
await expect(page.getByText('Herr Maier')).toBeVisible()
|
||||
|
||||
acceptNextDialog(page)
|
||||
// Löschen-Button im Karten-Block von Herr Maier (zweiter Eintrag).
|
||||
const card = page.locator('li', { hasText: 'Herr Maier' })
|
||||
await card.getByRole('button', { name: t.delete }).click()
|
||||
|
||||
await expect(page.getByText(de.common.deleted)).toBeVisible()
|
||||
await expect(page.getByText('Herr Maier')).toHaveCount(0)
|
||||
expect(mockDb!.waitingList.find((e) => e.id === 'wl-seed-2')).toBeUndefined()
|
||||
})
|
||||
Reference in New Issue
Block a user