diff --git a/gerbil-manager-web/src/api/gerbils.ts b/gerbil-manager-web/src/api/gerbils.ts index 184bbc4..f43ba0a 100644 --- a/gerbil-manager-web/src/api/gerbils.ts +++ b/gerbil-manager-web/src/api/gerbils.ts @@ -11,6 +11,11 @@ export function getGerbil(id: string): Promise { return api.get(`${resources.gerbils}/${id}`) } +/** SEARCH-2b: distinct non-empty originBreeder (Herkunft) values, sorted. */ +export function listOriginBreeders(): Promise { + return api.get(`${resources.gerbils}/breeders`) +} + export function createGerbil(body: CreateGerbil): Promise { return api.post(resources.gerbils, body) } diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts index a59aa3e..9f58a61 100644 --- a/gerbil-manager-web/src/api/types.ts +++ b/gerbil-manager-web/src/api/types.ts @@ -31,6 +31,13 @@ export interface Gerbil { colorVarietyId: string | null originContactId: string | null receiverContactId: string | null + /** SEARCH-1/2b: free-text Herkunft (Züchter) from the import; Gridify-filterable. */ + originBreeder?: string | null + /** + * SEARCH-1/2b: normalized name (lowercase, [\s._-] stripped) — filter-only, + * not part of the returned payload; present here so it can be a Gridify field. + */ + nameSearch?: string | null /** Compact GEN-1 genotype string, e.g. "Aa CC Dd EE GG Pp Spsp rere" (or "?"-wildcards). */ genotype: string | null notes: string | null @@ -50,6 +57,7 @@ export interface CreateGerbil { colorVarietyId?: string | null originContactId?: string | null receiverContactId?: string | null + originBreeder?: string | null genotype?: string | null notes?: string | null } diff --git a/gerbil-manager-web/src/pages/GerbilsPage.tsx b/gerbil-manager-web/src/pages/GerbilsPage.tsx index 08691bf..5044ea2 100644 --- a/gerbil-manager-web/src/pages/GerbilsPage.tsx +++ b/gerbil-manager-web/src/pages/GerbilsPage.tsx @@ -1,8 +1,8 @@ import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { de } from '../strings/de' -import { listGerbils, updateGerbil } from '../api/gerbils' -import { listColorVarieties, listContacts } from '../api/lookups' +import { listGerbils, listOriginBreeders, updateGerbil } from '../api/gerbils' +import { listColorVarieties } from '../api/lookups' import { andFilter, condition, type GridifyQuery } from '../api/gridify' import { GENDERS, GERBIL_STATUSES, type Gender, type GerbilStatus } from '../api/types' import { useApi, useMutation } from '../hooks/useApi' @@ -11,10 +11,11 @@ import './gerbils.css' const PAGE_SIZE = 20 -// SEARCH-2: name search field. Switches to Pam's separator-insensitive -// normalized field once SEARCH-1 lands (see conversation SEARCH-1); 'name' -// works today via the contains hotfix. -const NAME_SEARCH_FIELD = 'name' +// SEARCH-2b: separator-insensitive search via Pam's normalized field. The term +// is normalized the SAME way server-side stores it (lowercase + strip [\s._-]), +// so 'clan kleine chaoten' matches 'clan-kleine-chaoten'. +const NAME_SEARCH_FIELD = 'nameSearch' +const normalizeSearch = (s: string): string => s.toLowerCase().replace(/[\s._-]/g, '') type SortKey = 'nameAsc' | 'nameDesc' | 'birthDesc' | 'birthAsc' const SORT_ORDER_BY: Record = { @@ -30,7 +31,7 @@ export default function GerbilsPage() { const [status, setStatus] = useState('Active') const [gender, setGender] = useState('') const [colorVarietyId, setColorVarietyId] = useState('') - const [originContactId, setOriginContactId] = useState('') + const [originBreeder, setOriginBreeder] = useState('') const [sort, setSort] = useState('nameAsc') const [page, setPage] = useState(1) @@ -40,14 +41,15 @@ export default function GerbilsPage() { for (const cv of colorVarieties.data ?? []) map.set(cv.id, cv.name) return map }, [colorVarieties.data]) - const contacts = useApi(() => listContacts(), []) + const breeders = useApi(() => listOriginBreeders(), []) const filter = andFilter( - search.trim() && condition({ field: NAME_SEARCH_FIELD, op: 'contains', value: search.trim() }), + search.trim() && + condition({ field: NAME_SEARCH_FIELD, op: 'contains', value: normalizeSearch(search) }), status && condition({ field: 'status', op: '==', value: status }), gender && condition({ field: 'gender', op: '==', value: gender }), colorVarietyId && condition({ field: 'colorVarietyId', op: '==', value: colorVarietyId }), - originContactId && condition({ field: 'originContactId', op: '==', value: originContactId }), + originBreeder && condition({ field: 'originBreeder', op: '==', value: originBreeder }), ) const orderBy = SORT_ORDER_BY[sort] const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE } @@ -59,7 +61,7 @@ export default function GerbilsPage() { setStatus('Active') setGender('') setColorVarietyId('') - setOriginContactId('') + setOriginBreeder('') setPage(1) setSort('nameAsc') } @@ -164,13 +166,13 @@ export default function GerbilsPage() {