FEAT-3: Wurf detail workspace (parents, juveniles, Jungtier erfassen prefill, expected Farbschlag via breed)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
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 { de } from '../strings/de'
|
||||||
import { createGerbil, getGerbil, updateGerbil } from '../api/gerbils'
|
import { createGerbil, getGerbil, updateGerbil } from '../api/gerbils'
|
||||||
import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups'
|
import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups'
|
||||||
@@ -112,6 +112,22 @@ export default function GerbilFormPage() {
|
|||||||
setForm(formFromGerbil(existing.data))
|
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 = <K extends keyof FormState>(key: K, value: FormState[K]) =>
|
const set = <K extends keyof FormState>(key: K, value: FormState[K]) =>
|
||||||
setForm((f) => ({ ...f, [key]: value }))
|
setForm((f) => ({ ...f, [key]: value }))
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,139 @@
|
|||||||
|
import { useMemo } from 'react'
|
||||||
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
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() {
|
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 <p className="muted">{de.common.loading}</p>
|
||||||
|
if (litter.error || !litter.data) {
|
||||||
|
return (
|
||||||
|
<section className="page">
|
||||||
|
<p className="muted">{litter.error ?? t.detail.notFound}</p>
|
||||||
|
<Link to="/wuerfe" className="btn">
|
||||||
|
{t.detail.back}
|
||||||
|
</Link>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const l = litter.data
|
||||||
|
const registered = juveniles.data?.totalCount ?? 0
|
||||||
|
const parentLink = (id: string | null, g: { name: string } | null) =>
|
||||||
|
id && g ? <Link to={`/rennmaeuse/${id}`}>{g.name}</Link> : t.detail.unknownParent
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page">
|
<section className="page">
|
||||||
<h2>{de.pages.litters.title}</h2>
|
<header className="page-head">
|
||||||
|
<h2>{l.name}</h2>
|
||||||
|
<div className="head-actions">
|
||||||
|
<Link to={`/wuerfe/${l.id}/bearbeiten`} className="btn btn--primary">
|
||||||
|
{t.detail.edit}
|
||||||
|
</Link>
|
||||||
|
<Link to="/wuerfe" className="btn">
|
||||||
|
{t.detail.back}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<dl className="def-list">
|
||||||
|
<div className="def-row">
|
||||||
|
<dt>{t.fields.date}</dt>
|
||||||
|
<dd>{formatDate(l.date)}</dd>
|
||||||
|
</div>
|
||||||
|
<div className="def-row">
|
||||||
|
<dt>{t.detail.parents}</dt>
|
||||||
|
<dd>
|
||||||
|
{parentLink(fatherId, father.data)} × {parentLink(motherId, mother.data)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="def-row">
|
||||||
|
<dt>{t.detail.registered}</dt>
|
||||||
|
<dd>
|
||||||
|
{registered}
|
||||||
|
{l.totalBorn != null ? ` / ${l.totalBorn}` : ''}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="def-row">
|
||||||
|
<dt>{t.fields.expectedGoHomeDate}</dt>
|
||||||
|
<dd>{formatDate(l.expectedGoHomeDate)}</dd>
|
||||||
|
</div>
|
||||||
|
{l.notes && (
|
||||||
|
<div className="def-row">
|
||||||
|
<dt>{t.fields.notes}</dt>
|
||||||
|
<dd>{l.notes}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div className="page-head">
|
||||||
|
<h3>{t.detail.juveniles}</h3>
|
||||||
|
<Link
|
||||||
|
to={`/rennmaeuse/neu?litterId=${l.id}${l.date ? `&dob=${l.date}` : ''}`}
|
||||||
|
className="btn btn--primary"
|
||||||
|
>
|
||||||
|
+ {t.detail.registerJuvenile}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{juveniles.loading && <p className="muted">{de.common.loading}</p>}
|
||||||
|
{!juveniles.loading && registered === 0 && <p className="muted">{t.detail.noJuveniles}</p>}
|
||||||
|
{registered > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{(juveniles.data?.items ?? []).map((g) => (
|
||||||
|
<li key={g.id}>
|
||||||
|
<Link to={`/rennmaeuse/${g.id}`} className="gerbil-card">
|
||||||
|
<span className="gerbil-card__name">{g.name}</span>
|
||||||
|
<span className="gerbil-card__meta">{de.pages.gerbils.genderLabels[g.gender]}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<h3>{t.detail.expectedColors}</h3>
|
||||||
|
{expected ? (
|
||||||
|
<BreedingResultView result={expected} title={t.detail.expectedColors} />
|
||||||
|
) : (
|
||||||
|
<p className="muted">{t.detail.needParentsGenotype}</p>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user