feat(tiere): Eltern direkt in der Tier-Akte bearbeiten (QOL)
All checks were successful
CI / Backend Tests (.NET) (push) Successful in 1m15s
CI / Frontend Tests (Node/Vite) (push) Successful in 9m37s
CI / Docker Build & Push (push) Successful in 7m36s
CI / Deploy auf TrueNAS (Custom App) (push) Successful in 8s

Im Tier-Formular gibt es jetzt den Block „Abstammung" mit Wurf-Auswahl und
Vater-/Mutter-Picker — die Eltern müssen nicht mehr über die Wurf-Seite
gesucht und dort editiert werden. Das Datenmodell bleibt unverändert: Eltern
hängen weiterhin am Geburtswurf.

- Backend: PUT /gerbils/{id}/parents schreibt Litter.FatherId/MotherId des
  Geburtswurfs. Ohne Wurf wird ein bestehender mit gleichem Elternpaar +
  gleichem Datum verknüpft, sonst ein Träger-Wurf angelegt
  ("Wurf von X + Y", ShowInChronicle=false, IsManual=true).
  Geschlechts-Regel wiederverwendet LitterEndpoints.ValidateParents,
  Selbstbezug (Tier als eigener Elternteil) wird abgewiesen.
- UI: Vorbelegung aus dem gewählten Wurf, Warnung mit Anzahl der Geschwister
  (Eltern gehören dem Wurf → Änderung gilt für alle), Hinweis wenn ein
  Wurf-Eintrag angelegt wird. Texte in de.ts.
- Nebenbei: Speichern nutzt im Edit-Modus die Route-Id (PUT /gerbils/{id}
  antwortet 204 ohne Body) und der vorher schon rote Spec-Locator
  „Würfe als Elternteil" ist auf den Abschnitt eingegrenzt.
- Tests: GerbilParentsTests (9), e2e tiere.spec (2 neu) + Mock-Route.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 20:43:28 +02:00
parent 7a2feac04d
commit 54e873c115
11 changed files with 653 additions and 29 deletions

View File

@@ -936,6 +936,79 @@ export async function installMockApi(page: Page): Promise<MockDb> {
return json(route, 200, results)
}
// QOL-ELTERN: Eltern direkt an der Tier-Akte setzen (PUT /gerbils/:id/parents).
// Spiegelt das Backend: Eltern werden am GEBURTSWURF gespeichert; fehlt der Wurf, wird
// ein bestehender mit gleichen Eltern + gleichem Datum verknüpft, sonst einer angelegt
// (showInChronicle=false → nicht in der Wurfchronik).
const parentsMatch = path.match(/^\/gerbils\/([^/]+)\/parents$/)
if (parentsMatch) {
if (method !== 'PUT') return json(route, 405)
const gid = decodeURIComponent(parentsMatch[1])
const g = db.gerbils.find((x) => x.id === gid)
if (!g) return json(route, 404, { title: 'Not Found' })
const body = (request.postDataJSON() ?? {}) as {
fatherId?: string | null
motherId?: string | null
}
const fatherId = body.fatherId ?? null
const motherId = body.motherId ?? null
const father = fatherId ? db.gerbils.find((x) => x.id === fatherId) : null
const mother = motherId ? db.gerbils.find((x) => x.id === motherId) : null
if (father?.gender === 'female' || mother?.gender === 'male') {
return json(route, 400, {
code: 'InvalidParentGender',
fatherGender: father?.gender ?? 'unknown',
motherGender: mother?.gender ?? 'unknown',
})
}
let litter = g.litterId ? (db.litters.find((l) => l.id === g.litterId) ?? null) : null
let litterCreated = false
let litterAttached = false
if (!litter && !fatherId && !motherId) {
return json(route, 200, {
litterId: null, litterName: null, litterCreated: false, litterAttached: false,
siblingCount: 0, fatherId: null, motherId: null,
})
}
if (!litter) {
if (g.dateOfBirth) {
litter =
db.litters.find(
(l) => l.fatherId === fatherId && l.motherId === motherId && l.date === g.dateOfBirth,
) ?? null
litterAttached = litter !== null
}
if (!litter) {
litter = {
id: newId('litter'),
name: `Wurf von ${father?.name ?? '—'} + ${mother?.name ?? '—'}`,
date: g.dateOfBirth ?? '',
totalBorn: null,
expectedGoHomeDate: null,
notes: null,
fatherId,
motherId,
showInChronicle: false,
}
db.litters.push(litter)
litterCreated = true
}
g.litterId = litter.id
}
litter.fatherId = fatherId
litter.motherId = motherId
const siblingCount = db.gerbils.filter((x) => x.litterId === litter!.id && x.id !== gid).length
return json(route, 200, {
litterId: litter.id,
litterName: litter.name,
litterCreated,
litterAttached,
siblingCount,
fatherId,
motherId,
})
}
// Generische Kollektionen: /<resource> und /<resource>/<id>
m = path.match(/^\/([a-z-]+)(?:\/([^/]+))?$/)
const col = m ? collections[m[1]] : undefined