207 lines
7.5 KiB
TypeScript
207 lines
7.5 KiB
TypeScript
import { useMemo, useState, type ReactNode } from 'react'
|
|
import { Link, useParams } from 'react-router-dom'
|
|
import { de } from '../strings/de'
|
|
import { getGerbil, updateGerbil } from '../api/gerbils'
|
|
import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups'
|
|
import { useApi, useMutation } from '../hooks/useApi'
|
|
import { formatDate, genderLabel, statusLabel } from '../format/labels'
|
|
import { fromDisplayString, genotypeToFarbschlag, toDisplayString } from '../genetics'
|
|
import FarbschlagImage from '../components/FarbschlagImage'
|
|
// FEAT-6 (Oscar): Tab-Inhalte + Profilfoto
|
|
import GerbilHealthTab from '../components/GerbilHealthTab'
|
|
import GerbilPhotosTab from '../components/GerbilPhotosTab'
|
|
import GerbilProfilePhoto from '../components/GerbilProfilePhoto'
|
|
import GerbilWeightTab from '../components/GerbilWeightTab'
|
|
|
|
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 forSale = useMutation(() => updateGerbil(id, { status: 'ForSale' }))
|
|
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) ?? '—') : '—')
|
|
const storedColorName = g.colorVarietyId ? (colorName.get(g.colorVarietyId) ?? null) : null
|
|
|
|
return (
|
|
<section className="page">
|
|
<header className="page-head">
|
|
<div>
|
|
<GerbilProfilePhoto gerbilId={g.id} />
|
|
<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={`/genetik?parent=${g.id}`} className="btn">
|
|
{t.detail.testMating}
|
|
</Link>
|
|
{g.status !== 'ForSale' && (
|
|
<button
|
|
type="button"
|
|
className="btn"
|
|
disabled={forSale.pending}
|
|
onClick={async () => {
|
|
const r = await forSale.run()
|
|
if (r.ok) gerbil.reload()
|
|
}}
|
|
>
|
|
{de.pages.abgabe.markAction}
|
|
</button>
|
|
)}
|
|
<Link to={`/rennmaeuse/${g.id}/stammbaum`} className="btn">
|
|
{de.pages.stammbaum.openButton}
|
|
</Link>
|
|
{/* FEAT-13: Abgabe abschließen — Vertrag-Assistent mit diesem Tier vorausgewählt. */}
|
|
{g.status !== 'Deceased' && g.status !== 'GivenAway' && (
|
|
<Link to={`/vertraege/neu?tiere=${g.id}`} className="btn">
|
|
{de.pages.vertraege.wizard.title}
|
|
</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={
|
|
storedColorName ? (
|
|
<span className="farbschlag-value">
|
|
<FarbschlagImage name={storedColorName} size={32} />
|
|
{storedColorName}
|
|
</span>
|
|
) : (
|
|
'—'
|
|
)
|
|
}
|
|
/>
|
|
<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.resolvedPrefix}
|
|
value={
|
|
<>
|
|
{geno.farbschlag}
|
|
{storedColorName &&
|
|
geno.farbschlag !== de.genetics.unknownFarbschlag &&
|
|
storedColorName !== geno.farbschlag && (
|
|
<small className="mismatch-hint"> ⚠ {t.detail.farbschlagMismatch}</small>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
</dl>
|
|
) : (
|
|
<p className="muted">{t.detail.genotypeNotSet}</p>
|
|
)}
|
|
|
|
<h3 className="visually-hidden">{t.detail.moreData}</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">
|
|
{tab === 'photos' && <GerbilPhotosTab gerbilId={g.id} />}
|
|
{tab === 'health' && <GerbilHealthTab gerbilId={g.id} />}
|
|
{tab === 'weight' && <GerbilWeightTab gerbilId={g.id} />}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|