From e76554342bb94d1c3ec8d53bb468fb05b22cf640 Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 00:32:44 +0200 Subject: [PATCH] =?UTF-8?q?FEAT-3:=20Zuchtpaar-=C3=9Cbersicht=20tab=20(der?= =?UTF-8?q?ive=20pairs=20from=20litters)=20+=20=3Fvater/&mutter=20pair=20p?= =?UTF-8?q?refill=20in=20Probeverpaarung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- gerbil-manager-web/src/pages/GenetikPage.tsx | 38 ++- .../src/pages/WuerfeListPage.tsx | 261 +++++++++++------- 2 files changed, 194 insertions(+), 105 deletions(-) diff --git a/gerbil-manager-web/src/pages/GenetikPage.tsx b/gerbil-manager-web/src/pages/GenetikPage.tsx index a23d7bb..7a70f74 100644 --- a/gerbil-manager-web/src/pages/GenetikPage.tsx +++ b/gerbil-manager-web/src/pages/GenetikPage.tsx @@ -16,20 +16,34 @@ export default function GenetikPage() { const [father, setFather] = useState(EMPTY_PARENT) const [mother, setMother] = useState(EMPTY_PARENT) - // Pre-fill a parent when arriving from an animal's detail page (?parent=). - // Gender decides the side: female -> Mutter, otherwise Vater. + // Pre-fill parents from query params: + // ?parent= single animal from its detail page (gender picks side) + // ?vater=&mutter= a whole pair, e.g. from the Zuchtpaar overview const [searchParams] = useSearchParams() const parentId = searchParams.get('parent') - const prefill = useApi( - () => (parentId ? getGerbil(parentId) : Promise.resolve(null)), - [parentId], - ) - const [prefilledFor, setPrefilledFor] = useState(null) - if (prefill.data && prefilledFor !== prefill.data.id) { - setPrefilledFor(prefill.data.id) - const value: ParentValue = { genotype: prefill.data.genotype ?? '', animalName: prefill.data.name } - if (prefill.data.gender === 'female') setMother(value) - else setFather(value) + const vaterId = searchParams.get('vater') + const mutterId = searchParams.get('mutter') + + const prefill = useApi(() => (parentId ? getGerbil(parentId) : Promise.resolve(null)), [parentId]) + const prefillVater = useApi(() => (vaterId ? getGerbil(vaterId) : Promise.resolve(null)), [vaterId]) + const prefillMutter = useApi(() => (mutterId ? getGerbil(mutterId) : Promise.resolve(null)), [mutterId]) + + const [prefilledKey, setPrefilledKey] = useState(null) + const prefillKey = `${parentId ?? ''}|${vaterId ?? ''}|${mutterId ?? ''}` + const allReady = + (!parentId || prefill.data) && (!vaterId || prefillVater.data) && (!mutterId || prefillMutter.data) + if (prefillKey !== '||' && allReady && prefilledKey !== prefillKey) { + setPrefilledKey(prefillKey) + const toValue = (g: { genotype: string | null; name: string }): ParentValue => ({ + genotype: g.genotype ?? '', + animalName: g.name, + }) + if (prefillVater.data) setFather(toValue(prefillVater.data)) + if (prefillMutter.data) setMother(toValue(prefillMutter.data)) + if (prefill.data) { + if (prefill.data.gender === 'female') setMother(toValue(prefill.data)) + else setFather(toValue(prefill.data)) + } } const bothValid = isValidGenotype(father.genotype) && isValidGenotype(mother.genotype) diff --git a/gerbil-manager-web/src/pages/WuerfeListPage.tsx b/gerbil-manager-web/src/pages/WuerfeListPage.tsx index fe9a696..58fce9c 100644 --- a/gerbil-manager-web/src/pages/WuerfeListPage.tsx +++ b/gerbil-manager-web/src/pages/WuerfeListPage.tsx @@ -4,37 +4,62 @@ import { de } from '../strings/de' import { listLitters } from '../api/litters' import { listGerbils } from '../api/gerbils' import { andFilter, condition, type GridifyQuery } from '../api/gridify' +import type { Litter } from '../api/types' import { useApi } from '../hooks/useApi' import { formatDate } from '../format/labels' const PAGE_SIZE = 20 +type Tab = 'litters' | 'pairs' type SortKey = 'dateDesc' | 'dateAsc' -const SORT_ORDER_BY: Record = { - dateDesc: 'date desc', - dateAsc: 'date', -} +const SORT_ORDER_BY: Record = { dateDesc: 'date desc', dateAsc: 'date' } -/** Years offered in the filter: current year back through 15 years. */ function yearOptions(): number[] { const current = new Date().getFullYear() return Array.from({ length: 16 }, (_, i) => current - i) } +interface Pair { + fatherId: string + motherId: string + count: number + lastDate: string +} + +/** Group litters (with both parents known) by (father, mother). */ +function derivePairs(litters: Litter[]): Pair[] { + const map = new Map() + for (const l of litters) { + if (!l.fatherId || !l.motherId) continue + const key = `${l.fatherId}|${l.motherId}` + const existing = map.get(key) + if (existing) { + existing.count += 1 + if (l.date > existing.lastDate) existing.lastDate = l.date + } else { + map.set(key, { fatherId: l.fatherId, motherId: l.motherId, count: 1, lastDate: l.date }) + } + } + return Array.from(map.values()).sort((a, b) => b.lastDate.localeCompare(a.lastDate)) +} + export default function WuerfeListPage() { const t = de.pages.litters + const [tab, setTab] = useState('litters') const [year, setYear] = useState('') const [sort, setSort] = useState('dateDesc') const [page, setPage] = useState(1) - // Resolve parent names for the list rows. + // Parent names (shared by both tabs). const gerbils = useApi(() => listGerbils({ page: 1, pageSize: 1000, orderBy: 'name' }), []) const nameById = useMemo(() => { const map = new Map() for (const g of gerbils.data?.items ?? []) map.set(g.id, g.name) return map }, [gerbils.data]) + const parentName = (id: string | null) => (id ? (nameById.get(id) ?? t.detail.unknownParent) : '—') + // Litters tab (paged, filtered). const filter = andFilter( year && condition({ field: 'date', op: '>=', value: `${year}-01-01` }), year && condition({ field: 'date', op: '<=', value: `${year}-12-31` }), @@ -43,109 +68,159 @@ export default function WuerfeListPage() { const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE } const litters = useApi(() => listLitters(query), [filter, orderBy, page]) + // Pairs tab: all litters, grouped client-side. + const allLitters = useApi( + () => (tab === 'pairs' ? listLitters({ page: 1, pageSize: 1000, orderBy: 'date desc' }) : Promise.resolve(null)), + [tab], + ) + const pairs = useMemo(() => derivePairs(allLitters.data?.items ?? []), [allLitters.data]) + const total = litters.data?.totalCount ?? 0 const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) const items = litters.data?.items ?? [] - const parentName = (id: string | null) => (id ? (nameById.get(id) ?? t.detail.unknownParent) : '—') return (
-
-

{t.title}

- {litters.data && ( -

- {total} {t.countLabel} -

- )} -
+

{t.title}

+ {t.newButton}
-
- - +
+ +
- {litters.loading &&

{de.common.loading}

} - {litters.error && ( -
- {litters.error} - -
+ {tab === 'litters' && ( + <> +
+ + +
+ + {litters.loading &&

{de.common.loading}

} + {litters.error && ( +
+ {litters.error} + +
+ )} + {!litters.loading && !litters.error && items.length === 0 && ( +

{t.empty}

+ )} + + {items.length > 0 && ( +
    + {items.map((l) => ( +
  • + + {l.name} + {formatDate(l.date)} + + {parentName(l.fatherId)} × {parentName(l.motherId)} + + + {l.totalBorn != null ? `${l.totalBorn} ${t.fields.totalBorn}` : '—'} + + +
  • + ))} +
+ )} + + {totalPages > 1 && ( + + )} + )} - {!litters.loading && !litters.error && items.length === 0 && ( -

{t.empty}

- )} - - {items.length > 0 && ( -
    - {items.map((l) => ( -
  • - - {l.name} - {formatDate(l.date)} - - {parentName(l.fatherId)} × {parentName(l.motherId)} - - - {l.totalBorn != null ? `${l.totalBorn} ${t.fields.totalBorn}` : '—'} - - -
  • - ))} -
- )} - - {totalPages > 1 && ( - + {tab === 'pairs' && ( + <> + {allLitters.loading &&

{de.common.loading}

} + {!allLitters.loading && pairs.length === 0 &&

{t.pairs.empty}

} + {pairs.length > 0 && ( +
    + {pairs.map((p) => ( +
  • + + {parentName(p.fatherId)} × {parentName(p.motherId)} + + + {p.count} {t.pairs.litterCount} + + + {t.pairs.lastLitter}: {formatDate(p.lastDate)} + + + {t.pairs.testMating} + +
  • + ))} +
+ )} + )}
)