FEAT-1: Tiere detail page — Stammdaten, GEN-1 genotype+Farbschlag, placeholder tabs

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-05 23:55:54 +02:00
parent 9b586dd7fa
commit 0237f5de5e
2 changed files with 230 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
import { useMemo, useState, type ReactNode } from 'react'
import { Link, useParams } from 'react-router-dom'
import { de } from '../strings/de'
import { getGerbil } from '../api/gerbils'
import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups'
import { useApi } from '../hooks/useApi'
import { formatDate, genderLabel, statusLabel } from '../format/labels'
import { fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics'
type DetailTab = 'photos' | 'health' | 'weight'
/** Parse a stored genotype string and derive its normalized form + Farbschlag. */
function describeGenotype(genotype: string | null): { display: string; farbschlag: string } | null {
if (!genotype || !genotype.trim()) return null
try {
const g = fromDisplayString(genotype)
return { display: toDisplayString(g), farbschlag: genotypeToFarbschlag(g) }
} catch {
// Unparseable: show the raw value, no derived Farbschlag.
return { display: genotype, farbschlag: de.genetics.unknownFarbschlag }
}
}
function Row({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="def-row">
<dt>{label}</dt>
<dd>{value ?? '—'}</dd>
</div>
)
}
export default function GerbilDetailPage() {
const t = de.pages.gerbils
const { id = '' } = useParams()
const [tab, setTab] = useState<DetailTab>('photos')
const gerbil = useApi(() => getGerbil(id), [id])
const colorVarieties = useApi(() => listColorVarieties(), [])
const enclosures = useApi(() => listEnclosures(), [])
const contacts = useApi(() => listContacts(), [])
const litters = useApi(() => listLitters(), [])
const colorName = useMemo(
() => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])),
[colorVarieties.data],
)
const enclosureName = useMemo(
() => new Map((enclosures.data ?? []).map((e) => [e.id, e.name])),
[enclosures.data],
)
const contactName = useMemo(
() => new Map((contacts.data ?? []).map((c) => [c.id, c.name])),
[contacts.data],
)
const litterName = useMemo(
() => new Map((litters.data ?? []).map((l) => [l.id, l.name])),
[litters.data],
)
if (gerbil.loading) return <p className="muted">{de.common.loading}</p>
if (gerbil.error || !gerbil.data) {
return (
<section className="page">
<p className="muted">{gerbil.error ?? t.detail.notFound}</p>
<Link to="/rennmaeuse" className="btn">
{t.detail.back}
</Link>
</section>
)
}
const g = gerbil.data
const geno = describeGenotype(g.genotype)
const lookup = (map: Map<string, string>, key: string | null) => (key ? (map.get(key) ?? '—') : '—')
return (
<section className="page">
<header className="page-head">
<div>
<h2>{g.name}</h2>
<span className={`badge badge--${g.status.toLowerCase()}`}>{statusLabel(g.status)}</span>
</div>
<div className="head-actions">
<Link to={`/rennmaeuse/${g.id}/bearbeiten`} className="btn btn--primary">
{t.detail.edit}
</Link>
<Link to="/rennmaeuse" className="btn">
{t.detail.back}
</Link>
</div>
</header>
<h3>{t.detail.masterData}</h3>
<dl className="def-list">
<Row label={t.fields.gender} value={genderLabel(g.gender)} />
<Row label={t.fields.status} value={statusLabel(g.status)} />
<Row label={t.fields.dateOfBirth} value={formatDate(g.dateOfBirth)} />
{g.status === 'Deceased' && (
<>
<Row label={t.fields.dateOfDeath} value={formatDate(g.dateOfDeath)} />
<Row label={t.fields.causeOfDeath} value={g.causeOfDeath} />
</>
)}
{g.status === 'GivenAway' && (
<Row label={t.fields.goHomeDate} value={formatDate(g.goHomeDate)} />
)}
<Row label={t.fields.colorVariety} value={lookup(colorName, g.colorVarietyId)} />
<Row label={t.fields.enclosure} value={lookup(enclosureName, g.enclosureId)} />
<Row label={t.fields.litter} value={lookup(litterName, g.litterId)} />
<Row label={t.fields.origin} value={lookup(contactName, g.originContactId)} />
<Row label={t.fields.receiver} value={lookup(contactName, g.receiverContactId)} />
<Row label={t.fields.notes} value={g.notes} />
</dl>
<h3>{t.detail.genetics}</h3>
{geno ? (
<dl className="def-list">
<Row label={t.fields.genotype} value={<code>{geno.display}</code>} />
<Row label={t.detail.resolvedFarbschlag} value={geno.farbschlag} />
</dl>
) : (
<p className="muted">{t.detail.genotypeNotSet}</p>
)}
<h3 className="visually-hidden">Weitere Daten</h3>
<div className="tabs" role="tablist">
{(['photos', 'health', 'weight'] as const).map((key) => (
<button
key={key}
type="button"
role="tab"
aria-selected={tab === key}
className={tab === key ? 'tab tab--active' : 'tab'}
onClick={() => setTab(key)}
>
{t.detail.tabs[key]}
</button>
))}
</div>
<div className="tab-panel" role="tabpanel">
<p className="muted">{t.detail.tabPlaceholder}</p>
</div>
</section>
)
}