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 { listLitters as listLittersPaged } from '../api/litters' import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups' import { useApi, useMutation } from '../hooks/useApi' import { formatDate, genderLabel, statusLabel } from '../format/labels' import { ALL_TRAITS, TRAIT_CATEGORIES } from '../format/traits' import { fromDisplayString, genotypeToFarbschlag, displayGenotypeSafe } from '../genetics' import type { Gender, GerbilStatus } from '../api/types' import FarbschlagImage from '../components/FarbschlagImage' import GerbilHealthTab from '../components/GerbilHealthTab' import GerbilPhotosTab from '../components/GerbilPhotosTab' import GerbilProfilePhoto from '../components/GerbilProfilePhoto' import GerbilWeightTab from '../components/GerbilWeightTab' import { useGerbilName } from '../components/breederSuffix' import { useToast } from '../components/toast' import './rennmausakte.css' type DetailTab = 'photos' | 'health' | 'weight' // DESIGN (Rennmausakte): per-status colours for the hero status badge. const STATUS_COLORS: Record = { Breeding: '#5e8c5a', Pet: '#5a7da8', ForSale: '#c2703d', Deceased: '#8a847b', GivenAway: '#b08e5c', } const GENDER_SYMBOL: Record = { unknown: '?', male: '♂', female: '♀' } /** 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: displayGenotypeSafe(genotype), farbschlag: genotypeToFarbschlag(g) } } catch { return { display: displayGenotypeSafe(genotype), farbschlag: de.genetics.unknownFarbschlag } } } /** A labelled key/value row inside a card (semantic dt/dd, styled as a grid). */ function Kv({ label, children }: { label: string; children: ReactNode }) { return (
{label}
{children}
) } export default function GerbilDetailPage() { const t = de.pages.gerbils const gerbilName = useGerbilName() const toast = useToast() const { id = '' } = useParams() const [tab, setTab] = useState('photos') const gerbil = useApi(() => getGerbil(id), [id]) const forSale = useMutation(() => updateGerbil(id, { status: 'ForSale' })) // FEAT-14: Charakterbogen (persisted on the Gerbil). const [charTraits, setCharTraits] = useState([]) const [charNote, setCharNote] = useState('') const [charInit, setCharInit] = useState(null) // Charakter ist v. a. für lebende Bestandstiere relevant → bei Zucht/Liebhaber // aufgeklappt, sonst (Abgabe/Verstorben/Abgegeben) eingeklappt (manuell öffenbar). const [charExpanded, setCharExpanded] = useState(true) const saveCharacter = useMutation(() => updateGerbil(id, { characterTraits: charTraits, characterNote: charNote.trim() || null }), ) const colorVarieties = useApi(() => listColorVarieties(), []) const enclosures = useApi(() => listEnclosures(), []) const contacts = useApi(() => listContacts(), []) const litters = useApi(() => listLitters(), []) const parentLitters = useApi( () => listLittersPaged({ filter: `fatherId=${id}|motherId=${id}`, orderBy: 'date desc', pageSize: 50 }), [id], ) 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], ) // Parents come from the gerbil's own litter (litterId → litter.fatherId/motherId). // Fetch each parent gerbil for its name + link (mirrors WurfDetailPage). const ownLitterId = gerbil.data?.litterId ?? null const ownLitter = useMemo( () => (ownLitterId ? ((litters.data ?? []).find((l) => l.id === ownLitterId) ?? null) : null), [litters.data, ownLitterId], ) const fatherId = ownLitter?.fatherId ?? null const motherId = ownLitter?.motherId ?? null const father = useApi(() => (fatherId ? getGerbil(fatherId) : Promise.resolve(null)), [fatherId]) const mother = useApi(() => (motherId ? getGerbil(motherId) : Promise.resolve(null)), [motherId]) // Character chips — toggle while preserving the catalog order (stable output). const selectedTraits = new Set(charTraits) const toggleTrait = (key: string) => { const next = new Set(selectedTraits) if (next.has(key)) next.delete(key) else next.add(key) setCharTraits(ALL_TRAITS.filter((tr) => next.has(tr.key)).map((tr) => tr.key)) } if (gerbil.loading) return

{de.common.loading}

if (gerbil.error || !gerbil.data) { return (

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

{t.detail.back}
) } const g = gerbil.data // Abgegebene/verstorbene Tiere sind nicht mehr im eigenen Bestand → kein Gehege. const showEnclosure = g.status !== 'Deceased' && g.status !== 'GivenAway' const geno = describeGenotype(g.genotype) const lookup = (map: Map, key: string | null) => (key ? (map.get(key) ?? '—') : '—') const storedColorName = g.colorVarietyId ? (colorName.get(g.colorVarietyId) ?? null) : null // Geschwisterverpaarung: Vater und Mutter dieses Tiers stammen aus demselben // Wurf (Vollgeschwister) — erkennbar an gemeinsamer litterId der Eltern. const isSiblingPairing = !!father.data && !!mother.data && father.data.id !== mother.data.id && !!father.data.litterId && father.data.litterId === mother.data.litterId const parentLink = ( pid: string | null, p: Parameters[0] | null, ): ReactNode => pid && p ? ( {gerbilName(p) || de.pages.gerbils.nameless} ) : ( de.pages.litters.detail.unknownParent ) // Seed the character state once from the loaded gerbil (adjust-state-during-render). if (charInit !== g.id) { setCharInit(g.id) setCharTraits(g.characterTraits ?? []) setCharNote(g.characterNote ?? '') setCharExpanded(g.status === 'Breeding' || g.status === 'Pet') } return (
} />

{gerbilName(g) || de.pages.gerbils.nameless}

{statusLabel(g.status)} {g.isResident === false && ( {de.pages.gerbils.externalBadge} )}
{genderLabel(g.gender)} {g.isCastrated ? ' (Kastrat)' : ''} {g.dateOfBirth && ( {formatDate(g.dateOfBirth)} )} {storedColorName && ( {storedColorName} )} {isSiblingPairing && ( {t.detail.siblingPairing} )}
{t.detail.edit} {t.detail.testMating} {de.pages.stammbaum.openButton} {g.status !== 'ForSale' && g.status !== 'Deceased' && ( )} {g.status !== 'Deceased' && g.status !== 'GivenAway' && ( {de.pages.vertraege.wizard.title} )} {t.detail.back}

{t.detail.masterData}

{genderLabel(g.gender)} {g.isCastrated && (Kastrat)} {statusLabel(g.status)} {formatDate(g.dateOfBirth)} {g.status === 'Deceased' && ( <> {formatDate(g.dateOfDeath)} {g.causeOfDeath || '—'} )} {g.status === 'GivenAway' && ( {formatDate(g.goHomeDate)} )} {storedColorName ? ( <> {storedColorName} ) : ( '—' )} {g.spottingType && ( {de.pages.gerbils.spottingTypes[ g.spottingType as keyof typeof de.pages.gerbils.spottingTypes ] || g.spottingType} )} {showEnclosure && ( {lookup(enclosureName, g.enclosureId)} )} {g.litterId && litterName.has(g.litterId) ? ( {litterName.get(g.litterId)} ) : ( lookup(litterName, g.litterId) )} {ownLitter && ( <> {parentLink(fatherId, father.data)} {parentLink(motherId, mother.data)} )} {g.originContactId && contactName.has(g.originContactId) ? ( {contactName.get(g.originContactId)} ) : ( g.originBreeder || '—' )} {g.status !== 'Deceased' && ( {lookup(contactName, g.receiverContactId)} )} {g.notes || '—'}

{t.detail.genetics}

{geno ? (
{geno.display} {geno.farbschlag} {storedColorName && geno.farbschlag !== de.genetics.unknownFarbschlag && storedColorName !== geno.farbschlag.replace(' Schecke', '').replace(' Rex', '') && ( ⚠ {t.detail.farbschlagMismatch} )}
) : (

{t.detail.genotypeNotSet}

)}
{charExpanded && ( <> {TRAIT_CATEGORIES.map((cat) => { const count = cat.traits.filter((tr) => selectedTraits.has(tr.key)).length return (
{cat.category} {count > 0 && {count}}
{cat.traits.map((tr) => { const on = selectedTraits.has(tr.key) return ( ) })}
) })}