From 16a0016de966a8e003c8a4cb2c58419325f2eddd Mon Sep 17 00:00:00 2001 From: Gulum Date: Sat, 6 Jun 2026 00:07:32 +0200 Subject: [PATCH] FEAT-5: add gender-filtered animal pickers to Probeverpaarung parents Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/AnimalPicker.tsx | 73 +++++++++++++++++++ .../src/components/ParentSelector.tsx | 2 + gerbil-manager-web/src/index.css | 37 ++++++++++ gerbil-manager-web/src/pages/GenetikPage.tsx | 25 ++++++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 gerbil-manager-web/src/components/AnimalPicker.tsx diff --git a/gerbil-manager-web/src/components/AnimalPicker.tsx b/gerbil-manager-web/src/components/AnimalPicker.tsx new file mode 100644 index 0000000..052b24f --- /dev/null +++ b/gerbil-manager-web/src/components/AnimalPicker.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' +import { de } from '../strings/de' +import { listGerbils } from '../api/gerbils' +import { andFilter, condition } from '../api/gridify' +import type { Gender, Gerbil, Paged } from '../api/types' +import { useApi } from '../hooks/useApi' + +const EMPTY_PAGE: Paged = { items: [], totalCount: 0, page: 1, pageSize: 0 } + +export interface AnimalPickerProps { + /** Restrict results to this gender (Vater = male, Mutter = female). */ + gender: Gender + onPick: (gerbil: Gerbil) => void +} + +/** Search existing animals (gender-filtered) and pick one as a breeding parent. */ +export default function AnimalPicker({ gender, onPick }: AnimalPickerProps) { + const t = de.pages.genetik + const [search, setSearch] = useState('') + const [open, setOpen] = useState(false) + + const trimmed = search.trim() + const filter = andFilter( + trimmed && condition({ field: 'name', op: 'contains', value: trimmed }), + condition({ field: 'gender', op: '==', value: gender }), + ) + const result = useApi( + () => (trimmed ? listGerbils({ filter, orderBy: 'name', page: 1, pageSize: 8 }) : Promise.resolve(EMPTY_PAGE)), + [filter, trimmed], + ) + + const items = result.data?.items ?? [] + + return ( +
+ { + setSearch(e.target.value) + setOpen(true) + }} + onFocus={() => setOpen(true)} + aria-label={t.pickAnimal} + /> + {open && trimmed !== '' && ( +
    + {result.loading &&
  • {de.common.loading}
  • } + {!result.loading && items.length === 0 && ( +
  • {t.pickAnimalNone}
  • + )} + {items.map((g) => ( +
  • + +
  • + ))} +
+ )} +
+ ) +} diff --git a/gerbil-manager-web/src/components/ParentSelector.tsx b/gerbil-manager-web/src/components/ParentSelector.tsx index affb38a..907e220 100644 --- a/gerbil-manager-web/src/components/ParentSelector.tsx +++ b/gerbil-manager-web/src/components/ParentSelector.tsx @@ -53,6 +53,8 @@ export default function ParentSelector({ label, value, onChange, picker }: Paren {t.genotypePreview}: {preview} + ) : value.animalName ? ( + {t.pickAnimalNoGenotype} ) : ( {t.genotypeHint} )} diff --git a/gerbil-manager-web/src/index.css b/gerbil-manager-web/src/index.css index 0836633..2ad00a7 100644 --- a/gerbil-manager-web/src/index.css +++ b/gerbil-manager-web/src/index.css @@ -560,3 +560,40 @@ textarea { padding: 0.1rem 0.35rem; border-radius: 0.3rem; } + +.animal-picker { + position: relative; + margin-bottom: 0.5rem; +} + +.animal-picker__results { + list-style: none; + margin: 0.25rem 0 0; + padding: 0; + border: 1px solid var(--color-border); + border-radius: 0.5rem; + background: var(--color-surface); + max-height: 14rem; + overflow-y: auto; +} + +.animal-picker__item { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + padding: 0.5rem 0.75rem; + font: inherit; + color: var(--color-text); + cursor: pointer; + min-height: 44px; +} + +.animal-picker__item:hover { + background: var(--color-accent-soft); +} + +.animal-picker__hint { + padding: 0.5rem 0.75rem; +} diff --git a/gerbil-manager-web/src/pages/GenetikPage.tsx b/gerbil-manager-web/src/pages/GenetikPage.tsx index 9240f8d..2111dc5 100644 --- a/gerbil-manager-web/src/pages/GenetikPage.tsx +++ b/gerbil-manager-web/src/pages/GenetikPage.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react' import { de } from '../strings/de' import { breed, fromDisplayString, type BreedingResult } from '../genetics' import ParentSelector, { type ParentValue } from '../components/ParentSelector' +import AnimalPicker from '../components/AnimalPicker' import { isValidGenotype } from '../format/genotypeText' const EMPTY_PARENT: ParentValue = { genotype: '', animalName: null } @@ -32,8 +33,28 @@ export default function GenetikPage() {

{t.subtitle}

- - + setFather({ genotype: g.genotype ?? '', animalName: g.name })} + /> + } + /> + setMother({ genotype: g.genotype ?? '', animalName: g.name })} + /> + } + />
{!bothValid &&

{t.needBoth}

}