Merge branch 'worktree-agent-a36d202be311b9289'
# Conflicts: # GerbilManagerWebAPI/ApplicationContext.cs # GerbilManagerWebAPI/Program.cs # gerbil-manager-web/e2e/mock-data.ts
This commit is contained in:
@@ -577,6 +577,68 @@ export async function installMockApi(page: Page): Promise<MockDb> {
|
||||
return json(route, 405)
|
||||
}
|
||||
|
||||
// ABGABE-STATUS: /reservations — Reservierungs-/Abgabe-Status (verfügbar → reserviert → abgegeben).
|
||||
if (path === '/reservations') {
|
||||
const allowed = ['verfuegbar', 'reserviert', 'abgegeben']
|
||||
const normStatus = (s: unknown) =>
|
||||
typeof s === 'string' && allowed.includes(s.trim().toLowerCase()) ? s.trim().toLowerCase() : 'verfuegbar'
|
||||
if (method === 'POST') {
|
||||
const body = request.postDataJSON() as Row
|
||||
if (!body.gerbilId) return json(route, 400, 'GerbilId ist erforderlich.')
|
||||
const nowIso = new Date().toISOString()
|
||||
const created = {
|
||||
id: newId('reservation'),
|
||||
gerbilId: body.gerbilId,
|
||||
gerbilName: body.gerbilName ?? null,
|
||||
status: normStatus(body.status),
|
||||
reservedForContactId: body.reservedForContactId ?? null,
|
||||
contactName: body.contactName ?? null,
|
||||
appointmentDate: body.appointmentDate ?? null,
|
||||
price: body.price ?? null,
|
||||
note: body.note ?? null,
|
||||
handedOverDate: body.handedOverDate ?? null,
|
||||
createdAt: nowIso,
|
||||
updatedAt: nowIso,
|
||||
}
|
||||
db.reservations.push(created)
|
||||
return json(route, 201, created)
|
||||
}
|
||||
if (method === 'GET') {
|
||||
const gid = url.searchParams.get('gerbilId')
|
||||
const rows = gid ? db.reservations.filter((r) => r.gerbilId === gid) : [...db.reservations]
|
||||
return json(route, 200, rows.slice().reverse())
|
||||
}
|
||||
return json(route, 405)
|
||||
}
|
||||
const resm = path.match(/^\/reservations\/([^/]+)$/)
|
||||
if (resm) {
|
||||
const allowed = ['verfuegbar', 'reserviert', 'abgegeben']
|
||||
const normStatus = (s: unknown) =>
|
||||
typeof s === 'string' && allowed.includes(s.trim().toLowerCase()) ? s.trim().toLowerCase() : 'verfuegbar'
|
||||
const rid = decodeURIComponent(resm[1])
|
||||
const idx = db.reservations.findIndex((r) => r.id === rid)
|
||||
if (idx < 0) return json(route, 404, { title: 'Not Found' })
|
||||
if (method === 'PUT') {
|
||||
const body = request.postDataJSON() as Row
|
||||
const row = db.reservations[idx]
|
||||
if (typeof body.status === 'string') row.status = normStatus(body.status)
|
||||
if ('gerbilName' in body) row.gerbilName = body.gerbilName ?? null
|
||||
if ('contactName' in body) row.contactName = body.contactName ?? null
|
||||
if ('note' in body) row.note = body.note ?? null
|
||||
if ('reservedForContactId' in body) row.reservedForContactId = body.reservedForContactId ?? null
|
||||
if ('appointmentDate' in body) row.appointmentDate = body.appointmentDate ?? null
|
||||
if ('price' in body) row.price = body.price ?? null
|
||||
if ('handedOverDate' in body) row.handedOverDate = body.handedOverDate ?? null
|
||||
row.updatedAt = new Date().toISOString()
|
||||
return json(route, 200, row)
|
||||
}
|
||||
if (method === 'DELETE') {
|
||||
db.reservations.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
|
||||
|
||||
@@ -87,6 +87,8 @@ export interface MockDb {
|
||||
}
|
||||
// ERWERB: Erwerb/Kauf je Tier (Kaufdatum, Preis, Notiz) — /acquisitions
|
||||
acquisitions: Record<string, unknown>[]
|
||||
// ABGABE-STATUS: Reservierungs-/Abgabe-Status (POST/PUT/DELETE /reservations)
|
||||
reservations: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
function gerbil(
|
||||
@@ -544,5 +546,50 @@ export function seedDb(): MockDb {
|
||||
},
|
||||
},
|
||||
acquisitions: [],
|
||||
// ABGABE-STATUS: eine reservierte + eine abgegebene + eine verfügbare Vormerkung.
|
||||
reservations: [
|
||||
{
|
||||
id: 'res-reserviert',
|
||||
gerbilId: 'sale-balu',
|
||||
gerbilName: 'Balu Abgabe',
|
||||
status: 'reserviert',
|
||||
reservedForContactId: 'con-huber',
|
||||
contactName: 'Familie Huber',
|
||||
appointmentDate: '2026-07-01T00:00:00Z',
|
||||
price: 25,
|
||||
note: 'Käfig wird mitgebracht.',
|
||||
handedOverDate: null,
|
||||
createdAt: '2026-06-15T09:00:00Z',
|
||||
updatedAt: '2026-06-15T09:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'res-abgegeben',
|
||||
gerbilId: 'pup-abgabe',
|
||||
gerbilName: 'Pippa',
|
||||
status: 'abgegeben',
|
||||
reservedForContactId: 'con-huber',
|
||||
contactName: 'Familie Huber',
|
||||
appointmentDate: null,
|
||||
price: 20,
|
||||
note: null,
|
||||
handedOverDate: '2025-05-01T00:00:00Z',
|
||||
createdAt: '2025-04-20T09:00:00Z',
|
||||
updatedAt: '2025-05-01T09:00:00Z',
|
||||
},
|
||||
{
|
||||
id: 'res-verfuegbar',
|
||||
gerbilId: 'sale-benny',
|
||||
gerbilName: 'Benny Abgabe',
|
||||
status: 'verfuegbar',
|
||||
reservedForContactId: null,
|
||||
contactName: null,
|
||||
appointmentDate: null,
|
||||
price: null,
|
||||
note: null,
|
||||
handedOverDate: null,
|
||||
createdAt: '2026-06-16T09:00:00Z',
|
||||
updatedAt: '2026-06-16T09:00:00Z',
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
78
gerbil-manager-web/e2e/reservierungen.spec.ts
Normal file
78
gerbil-manager-web/e2e/reservierungen.spec.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ABGABE-STATUS: Reservierungs-/Abgabe-Status-Seite (verfügbar → reserviert → abgegeben).
|
||||
* Erreichbar über das Menü („Reservierungen"). Liste mit Status-Chips, Tier-/Kontakt-Links,
|
||||
* Anlegen, Status-Wechsel und Löschen.
|
||||
*/
|
||||
import { de, expect, gotoSection, skipUnlessMock, acceptNextDialog, test } from './fixtures'
|
||||
|
||||
const tr = de.pages.reservierungen
|
||||
|
||||
test.describe('Reservierungen', () => {
|
||||
test.beforeEach(() => skipUnlessMock())
|
||||
|
||||
test('über das Menü erreichbar, zeigt die Seed-Reservierungen', async ({ page }) => {
|
||||
await gotoSection(page, de.nav.reservations)
|
||||
await expect(page.getByRole('heading', { name: tr.title })).toBeVisible()
|
||||
|
||||
// Reservierte + abgegebene + verfügbare Vormerkung sichtbar.
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
await expect(page.getByText('Pippa')).toBeVisible()
|
||||
await expect(page.getByText('Benny Abgabe')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Status-Filter filtern korrekt (Anzahl-Badges stimmen)', async ({ page }) => {
|
||||
await page.goto('/reservierungen')
|
||||
|
||||
const filters = page.locator('.reservierungen-filter')
|
||||
await filters.filter({ hasText: tr.filters.reserviert }).click()
|
||||
await expect(page.getByText('Balu Abgabe')).toBeVisible()
|
||||
await expect(page.getByText('Benny Abgabe')).toHaveCount(0)
|
||||
|
||||
await filters.filter({ hasText: tr.filters.abgegeben }).click()
|
||||
await expect(page.getByText('Pippa')).toBeVisible()
|
||||
await expect(page.getByText('Balu Abgabe')).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Tier- und Kontakt-Bezüge sind verlinkt', async ({ page }) => {
|
||||
await page.goto('/reservierungen')
|
||||
const card = page.locator('.reservation-card').filter({ hasText: 'Balu Abgabe' })
|
||||
await expect(card.getByRole('link', { name: 'Balu Abgabe' })).toHaveAttribute('href', /\/rennmaeuse\/sale-balu/)
|
||||
await expect(card.getByRole('link', { name: 'Familie Huber' })).toHaveAttribute('href', /\/kontakte\/con-huber/)
|
||||
})
|
||||
|
||||
test('Status wechseln: verfügbares Tier reservieren', async ({ page }) => {
|
||||
await page.goto('/reservierungen')
|
||||
const card = page.locator('.reservation-card').filter({ hasText: 'Benny Abgabe' })
|
||||
await expect(card.locator('.reservation-badge--verfuegbar')).toBeVisible()
|
||||
|
||||
await card.getByRole('button', { name: tr.actions.markReserved }).click()
|
||||
await expect(
|
||||
page.locator('.reservation-card').filter({ hasText: 'Benny Abgabe' }).locator('.reservation-badge--reserviert'),
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('neue Reservierung anlegen', async ({ page }) => {
|
||||
await page.goto('/reservierungen')
|
||||
await page.getByRole('button', { name: tr.newButton }).click()
|
||||
|
||||
const form = page.locator('.reservation-form')
|
||||
await expect(form).toBeVisible()
|
||||
// Ein Tier auswählen (Krümel ist in den Seed-Daten vorhanden).
|
||||
await form.getByLabel(tr.form.animal).selectOption({ label: 'Krümel' })
|
||||
await form.getByRole('button', { name: tr.form.save }).click()
|
||||
|
||||
// Die neue Reservierung erscheint in der Liste.
|
||||
await expect(page.locator('.reservation-card').filter({ hasText: 'Krümel' })).toBeVisible()
|
||||
})
|
||||
|
||||
test('Reservierung löschen', async ({ page }) => {
|
||||
await page.goto('/reservierungen')
|
||||
const card = page.locator('.reservation-card').filter({ hasText: 'Benny Abgabe' })
|
||||
await expect(card).toBeVisible()
|
||||
|
||||
acceptNextDialog(page)
|
||||
await card.getByRole('button', { name: tr.actions.delete }).click()
|
||||
|
||||
await expect(page.locator('.reservation-card').filter({ hasText: 'Benny Abgabe' })).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user