import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { de } from '../strings/de' 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' import { formatDate, genderLabel, statusLabel } from '../format/labels' import { FilterPanel } from '../components/FilterPanel' import './gerbils.css' const PAGE_SIZE = 20 // 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 = { nameAsc: 'name', nameDesc: 'name desc', birthDesc: 'dateOfBirth desc', birthAsc: 'dateOfBirth', } export default function GerbilsPage() { const t = de.pages.gerbils const [search, setSearch] = useState('') const [status, setStatus] = useState('Active') const [gender, setGender] = useState('') const [colorVarietyId, setColorVarietyId] = useState('') const [originBreeder, setOriginBreeder] = useState('') // BESTAND-FILTER: default to the own clan (Bestand); opt in to external ancestors. const [showExternal, setShowExternal] = useState(false) const [sort, setSort] = useState('nameAsc') const [page, setPage] = useState(1) const colorVarieties = useApi(() => listColorVarieties(), []) const colorNameById = useMemo(() => { const map = new Map() for (const cv of colorVarieties.data ?? []) map.set(cv.id, cv.name) return map }, [colorVarieties.data]) const breeders = useApi(() => listOriginBreeders(), []) const filter = andFilter( 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 }), originBreeder && condition({ field: 'originBreeder', op: '==', value: originBreeder }), // Default view = own clan (Bestand). Unless "externe Ahnen einblenden" is on, // restrict to resident animals (server-side, so paging/counts stay correct). !showExternal && condition({ field: 'isResident', op: '==', value: true }), ) const orderBy = SORT_ORDER_BY[sort] const query: GridifyQuery = { filter: filter || undefined, orderBy, page, pageSize: PAGE_SIZE } const gerbils = useApi(() => listGerbils(query), [filter, orderBy, page]) const resetFilters = () => { setSearch('') setStatus('Active') setGender('') setColorVarietyId('') setOriginBreeder('') setShowExternal(false) setPage(1) setSort('nameAsc') } // When a filter changes, jump back to page 1. const onFilterChange = (setter: (v: T) => void) => (value: T) => { setter(value) setPage(1) } const total = gerbils.data?.totalCount ?? 0 const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)) const items = gerbils.data?.items ?? [] // UX-MOBILE-1: count non-default filter values for the badge. const activeFilterCount = (status !== 'Active' ? 1 : 0) + (gender !== '' ? 1 : 0) + (colorVarietyId !== '' ? 1 : 0) + (originBreeder !== '' ? 1 : 0) + (showExternal ? 1 : 0) // Multi-select bulk "Zur Abgabe stellen". const [selected, setSelected] = useState>(new Set()) const toggleSelect = (id: string) => setSelected((prev) => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) const bulkForSale = useMutation((ids: string[]) => Promise.all(ids.map((id) => updateGerbil(id, { status: 'ForSale' }))), ) async function markSelectedForSale() { const result = await bulkForSale.run([...selected]) if (result.ok) { setSelected(new Set()) gerbils.reload() } } return (

{t.title}

{gerbils.data && (

{total} {t.countLabel}

)}
+ {t.newButton}
onFilterChange(setSearch)(e.target.value)} aria-label={t.fields.name} /> } activeCount={activeFilterCount} onReset={resetFilters} > {gerbils.loading &&

{de.common.loading}

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

{t.empty}

)} {selected.size > 0 && (
{selected.size} {t.countLabel} {bulkForSale.error && {bulkForSale.error}}
)} {items.length > 0 && (
    {items.map((g) => (
  • toggleSelect(g.id)} aria-label={g.name || t.nameless} /> {g.name || t.nameless} {g.isResident === false && ( {t.externalBadge} )} {statusLabel(g.status)} {genderLabel(g.gender)} {g.colorVarietyId ? (colorNameById.get(g.colorVarietyId) ?? '—') : '—'} {formatDate(g.dateOfBirth)}
  • ))}
)} {totalPages > 1 && ( )}
) }