From 79f260d62e309219defb5e0a21b0a3b3ede0a135 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 00:31:19 +0200 Subject: [PATCH] FEAT-3: Wurf detail workspace (parents, juveniles, Jungtier erfassen prefill, expected Farbschlag via breed) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/pages/GerbilFormPage.tsx | 18 ++- .../src/pages/WurfDetailPage.tsx | 133 +++++++++++++++++- 2 files changed, 148 insertions(+), 3 deletions(-) diff --git a/gerbil-manager-web/src/pages/GerbilFormPage.tsx b/gerbil-manager-web/src/pages/GerbilFormPage.tsx index 7356c86..4bb3b27 100644 --- a/gerbil-manager-web/src/pages/GerbilFormPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilFormPage.tsx @@ -1,5 +1,5 @@ import { useState, type FormEvent } from 'react' -import { Link, useNavigate, useParams } from 'react-router-dom' +import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom' import { de } from '../strings/de' import { createGerbil, getGerbil, updateGerbil } from '../api/gerbils' import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups' @@ -112,6 +112,22 @@ export default function GerbilFormPage() { setForm(formFromGerbil(existing.data)) } + // Create-mode prefill from query params (?litterId, ?dob) — used by the Wurf + // workspace's "Jungtier erfassen" action to register a pup in one tap. + const [searchParams] = useSearchParams() + if (!isEdit && initializedFor === null) { + const litterId = searchParams.get('litterId') + const dob = searchParams.get('dob') + if (litterId || dob) { + setInitializedFor('new') + setForm((f) => ({ + ...f, + litterId: litterId ?? f.litterId, + dateOfBirth: dob ?? f.dateOfBirth, + })) + } + } + const set = (key: K, value: FormState[K]) => setForm((f) => ({ ...f, [key]: value })) diff --git a/gerbil-manager-web/src/pages/WurfDetailPage.tsx b/gerbil-manager-web/src/pages/WurfDetailPage.tsx index 0da05ca..5ecd556 100644 --- a/gerbil-manager-web/src/pages/WurfDetailPage.tsx +++ b/gerbil-manager-web/src/pages/WurfDetailPage.tsx @@ -1,10 +1,139 @@ +import { useMemo } from 'react' +import { Link, useParams } from 'react-router-dom' import { de } from '../strings/de' +import { getLitter } from '../api/litters' +import { getGerbil, listGerbils } from '../api/gerbils' +import { condition } from '../api/gridify' +import { useApi } from '../hooks/useApi' +import { formatDate } from '../format/labels' +import { isValidGenotype } from '../format/genotypeText' +import { breed, fromDisplayString, type BreedingResult } from '../genetics' +import BreedingResultView from '../components/BreedingResultView' export default function WurfDetailPage() { - // Fleshed out in a later FEAT-3 increment (litter workspace + Jungtier erfassen). + const t = de.pages.litters + const { id = '' } = useParams() + + const litter = useApi(() => getLitter(id), [id]) + const fatherId = litter.data?.fatherId ?? null + const motherId = litter.data?.motherId ?? null + + const father = useApi(() => (fatherId ? getGerbil(fatherId) : Promise.resolve(null)), [fatherId]) + const mother = useApi(() => (motherId ? getGerbil(motherId) : Promise.resolve(null)), [motherId]) + + // Juveniles = gerbils whose litterId is this litter. + const juveniles = useApi( + () => + id + ? listGerbils({ + filter: condition({ field: 'litterId', op: '==', value: id }), + orderBy: 'name', + page: 1, + pageSize: 1000, + }) + : Promise.resolve(null), + [id], + ) + + // Expected Farbschlag distribution for this pairing (client-side GEN-1). + const expected: BreedingResult | null = useMemo(() => { + const fg = father.data?.genotype + const mg = mother.data?.genotype + if (!fg || !mg || !isValidGenotype(fg) || !isValidGenotype(mg)) return null + return breed(fromDisplayString(fg), fromDisplayString(mg)) + }, [father.data, mother.data]) + + if (litter.loading) return

{de.common.loading}

+ if (litter.error || !litter.data) { + return ( +
+

{litter.error ?? t.detail.notFound}

+ + {t.detail.back} + +
+ ) + } + + const l = litter.data + const registered = juveniles.data?.totalCount ?? 0 + const parentLink = (id: string | null, g: { name: string } | null) => + id && g ? {g.name} : t.detail.unknownParent + return (
-

{de.pages.litters.title}

+
+

{l.name}

+
+ + {t.detail.edit} + + + {t.detail.back} + +
+
+ +
+
+
{t.fields.date}
+
{formatDate(l.date)}
+
+
+
{t.detail.parents}
+
+ {parentLink(fatherId, father.data)} × {parentLink(motherId, mother.data)} +
+
+
+
{t.detail.registered}
+
+ {registered} + {l.totalBorn != null ? ` / ${l.totalBorn}` : ''} +
+
+
+
{t.fields.expectedGoHomeDate}
+
{formatDate(l.expectedGoHomeDate)}
+
+ {l.notes && ( +
+
{t.fields.notes}
+
{l.notes}
+
+ )} +
+ +
+

{t.detail.juveniles}

+ + + {t.detail.registerJuvenile} + +
+ {juveniles.loading &&

{de.common.loading}

} + {!juveniles.loading && registered === 0 &&

{t.detail.noJuveniles}

} + {registered > 0 && ( +
    + {(juveniles.data?.items ?? []).map((g) => ( +
  • + + {g.name} + {de.pages.gerbils.genderLabels[g.gender]} + +
  • + ))} +
+ )} + +

{t.detail.expectedColors}

+ {expected ? ( + + ) : ( +

{t.detail.needParentsGenotype}

+ )}
) }