FEAT-5: add gender-filtered animal pickers to Probeverpaarung parents
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
73
gerbil-manager-web/src/components/AnimalPicker.tsx
Normal file
73
gerbil-manager-web/src/components/AnimalPicker.tsx
Normal file
@@ -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<Gerbil> = { 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 (
|
||||
<div className="animal-picker">
|
||||
<input
|
||||
type="search"
|
||||
className="input"
|
||||
placeholder={t.pickAnimalSearch}
|
||||
value={search}
|
||||
onChange={(e) => {
|
||||
setSearch(e.target.value)
|
||||
setOpen(true)
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
aria-label={t.pickAnimal}
|
||||
/>
|
||||
{open && trimmed !== '' && (
|
||||
<ul className="animal-picker__results">
|
||||
{result.loading && <li className="muted animal-picker__hint">{de.common.loading}</li>}
|
||||
{!result.loading && items.length === 0 && (
|
||||
<li className="muted animal-picker__hint">{t.pickAnimalNone}</li>
|
||||
)}
|
||||
{items.map((g) => (
|
||||
<li key={g.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="animal-picker__item"
|
||||
onClick={() => {
|
||||
onPick(g)
|
||||
setSearch('')
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{g.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -53,6 +53,8 @@ export default function ParentSelector({ label, value, onChange, picker }: Paren
|
||||
<small className="muted">
|
||||
{t.genotypePreview}: <strong>{preview}</strong>
|
||||
</small>
|
||||
) : value.animalName ? (
|
||||
<small className="error-text">{t.pickAnimalNoGenotype}</small>
|
||||
) : (
|
||||
<small className="muted">{t.genotypeHint}</small>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user