FEAT: Implement deceased/givenaway enclosure visibility rules, preserve external clan name, and hide receiver fields for deceased gerbils

This commit is contained in:
2026-06-13 01:40:19 +02:00
parent 5732398e60
commit 396d8f05d8
53 changed files with 7431 additions and 274 deletions

View File

@@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
import { de } from '../strings/de'
import { listLitters } from '../api/litters'
import { listGerbils } from '../api/gerbils'
import { andFilter, condition, type GridifyQuery } from '../api/gridify'
import { andFilter, condition, escapeGridifyValue, type GridifyQuery } from '../api/gridify'
import type { Litter } from '../api/types'
import { useApi } from '../hooks/useApi'
import { formatDate } from '../format/labels'
@@ -17,7 +17,7 @@ const SORT_ORDER_BY: Record<SortKey, string> = { dateDesc: 'date desc', dateAsc:
function yearOptions(): number[] {
const current = new Date().getFullYear()
return Array.from({ length: 16 }, (_, i) => current - i)
return Array.from({ length: 30 }, (_, i) => current - i)
}
interface Pair {
@@ -47,6 +47,7 @@ function derivePairs(litters: Litter[]): Pair[] {
export default function WuerfeListPage() {
const t = de.pages.litters
const [tab, setTab] = useState<Tab>('litters')
const [search, setSearch] = useState('')
const [year, setYear] = useState('')
const [sort, setSort] = useState<SortKey>('dateDesc')
const [page, setPage] = useState(1)
@@ -62,6 +63,7 @@ export default function WuerfeListPage() {
// Litters tab (paged, filtered).
const filter = andFilter(
search.trim() && `(name=*${escapeGridifyValue(search)}/i|notes=*${escapeGridifyValue(search)}/i|litterLetter=*${escapeGridifyValue(search)}/i)`,
year && condition({ field: 'date', op: '>=', value: `${year}-01-01` }),
year && condition({ field: 'date', op: '<=', value: `${year}-12-31` }),
)
@@ -76,6 +78,16 @@ export default function WuerfeListPage() {
)
const pairs = useMemo(() => derivePairs(allLitters.data?.items ?? []), [allLitters.data])
// Search-enabled pairs list (client-side search by parent name)
const filteredPairs = useMemo(() => {
if (!search.trim()) return pairs
const term = search.toLowerCase()
return pairs.filter((p) =>
parentName(p.fatherId).toLowerCase().includes(term) ||
parentName(p.motherId).toLowerCase().includes(term)
)
}, [pairs, search, nameById])
const total = litters.data?.totalCount ?? 0
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
const items = litters.data?.items ?? []
@@ -113,8 +125,26 @@ export default function WuerfeListPage() {
{tab === 'litters' && (
<>
<FilterPanel
activeCount={year !== '' ? 1 : 0}
onReset={() => { setYear(''); setSort('dateDesc'); setPage(1) }}
searchField={
<input
type="search"
className="input"
placeholder={t.searchPlaceholder}
value={search}
onChange={(e) => {
setSearch(e.target.value)
setPage(1)
}}
aria-label="Wurf suchen"
/>
}
activeCount={(year !== '' ? 1 : 0) + (search.trim() !== '' ? 1 : 0)}
onReset={() => {
setSearch('')
setYear('')
setSort('dateDesc')
setPage(1)
}}
>
<label className="field">
<span>{t.filterYear}</span>
@@ -202,11 +232,31 @@ export default function WuerfeListPage() {
{tab === 'pairs' && (
<>
<div style={{ marginBottom: '1.5rem', display: 'flex', gap: '1rem', alignItems: 'center' }}>
<input
type="search"
className="input"
style={{ maxWidth: '320px' }}
placeholder="Elternteil suchen …"
value={search}
onChange={(e) => setSearch(e.target.value)}
aria-label="Elternteil suchen"
/>
{search && (
<button
type="button"
className="btn btn--secondary btn--sm"
onClick={() => setSearch('')}
>
{de.pages.gerbils.filters.reset}
</button>
)}
</div>
{allLitters.loading && <p className="muted">{de.common.loading}</p>}
{!allLitters.loading && pairs.length === 0 && <p className="muted">{t.pairs.empty}</p>}
{pairs.length > 0 && (
{!allLitters.loading && filteredPairs.length === 0 && <p className="muted">{t.pairs.empty}</p>}
{filteredPairs.length > 0 && (
<ul className="card-list">
{pairs.map((p) => (
{filteredPairs.map((p) => (
<li key={`${p.fatherId}|${p.motherId}`} className="gerbil-card">
<span className="gerbil-card__name">
{parentName(p.fatherId)} × {parentName(p.motherId)}