BESTAND-FILTER: Tiere-Liste zeigt standardmäßig nur den eigenen Bestand

Die Tiere-Liste (/rennmaeuse) filtert serverseitig auf isResident==true
(eigener Clan = Bestand) und blendet externe Ahnen (nur für den Stammbaum
erfasste Fremdtiere) standardmäßig aus — passend zu Julians Modell
(Kleine Chaoten = Bestand, extern = stammbaum-only).

- Neuer Schalter „Externe Ahnen einblenden“ (Default aus). An -> isResident-
  Filter entfällt, externe Tiere erscheinen mit. Serverseitig (Gridify), damit
  Paging und Trefferzahl korrekt bleiben. „Filter zurücksetzen“ setzt ihn zurück.
- Subtile „Extern“-Markierung (gestricheltes Badge) auf externen Tieren in
  Liste UND Detailseite, mit Tooltip-Erläuterung.
- Gerbil-Typ um isResident (optional bool) ergänzt; CreateGerbil ebenso.

Deutsch via de.ts (LF), mobil + Laptop. e2e-Mock: gerbil()-Factory setzt
isResident=true; Hilde & Max als externe Ahnen (isResident=false). Neue
e2e bestand.spec (Default blendet aus; Toggle zeigt + Extern-Markierung).
Gate grün: build, eslint, vitest 78, e2e 84.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 11:59:18 +02:00
parent ae825ae80a
commit afd321ec9f
7 changed files with 94 additions and 4 deletions

View File

@@ -47,6 +47,13 @@ export interface Gerbil {
*/
characterTraits?: string[] | null
characterNote?: string | null
/**
* Residency (Bestand): true = part of the own clan (Kleine Chaoten), false =
* external/pedigree-only ancestor imported for the Stammbaum. Gridify-filterable
* (`isResident==true`); the Tiere list defaults to resident-only. Absent = treat
* as resident (backend default true).
*/
isResident?: boolean
}
/** Payload for POST /gerbils. */
@@ -68,6 +75,7 @@ export interface CreateGerbil {
notes?: string | null
characterTraits?: string[] | null
characterNote?: string | null
isResident?: boolean
}
/** Payload for PUT /gerbils/{id} (all optional / partial update). */

View File

@@ -330,6 +330,24 @@ textarea {
color: #3a6ea5;
}
/* BESTAND-FILTER: subtle marker for external (non-Bestand) animals. Inline next
to the name, so it should not be pushed to the grid edge like the status badge. */
.badge--external {
justify-self: start;
margin-left: 0.4rem;
vertical-align: middle;
background: #f0f0f0;
color: #777;
border: 1px dashed #bbb;
}
/* Checkbox-style filter (label text + box on one row), vs. the stacked .field. */
.field--check {
flex-direction: row;
align-items: center;
gap: 0.4rem;
}
@media (min-width: 768px) {
.gerbil-card {
grid-template-columns: 2fr 1fr 1.5fr 1fr auto;
@@ -741,4 +759,4 @@ textarea {
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--color-border);
}
}

View File

@@ -104,6 +104,11 @@ export default function GerbilDetailPage() {
<GerbilProfilePhoto gerbilId={g.id} />
<h2>{g.name}</h2>
<span className={`badge badge--${g.status.toLowerCase()}`}>{statusLabel(g.status)}</span>
{g.isResident === false && (
<span className="badge badge--external" title={de.pages.gerbils.externalTitle}>
{de.pages.gerbils.externalBadge}
</span>
)}
</div>
<div className="head-actions">
<Link to={`/rennmaeuse/${g.id}/bearbeiten`} className="btn btn--primary">

View File

@@ -32,6 +32,8 @@ export default function GerbilsPage() {
const [gender, setGender] = useState<Gender | ''>('')
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<SortKey>('nameAsc')
const [page, setPage] = useState(1)
@@ -50,6 +52,9 @@ export default function GerbilsPage() {
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 }
@@ -62,6 +67,7 @@ export default function GerbilsPage() {
setGender('')
setColorVarietyId('')
setOriginBreeder('')
setShowExternal(false)
setPage(1)
setSort('nameAsc')
}
@@ -186,6 +192,14 @@ export default function GerbilsPage() {
<option value="birthAsc">{t.sort.birthAsc}</option>
</select>
</label>
<label className="field field--check" title={t.filters.showExternalHint}>
<span>{t.filters.showExternal}</span>
<input
type="checkbox"
checked={showExternal}
onChange={(e) => onFilterChange(setShowExternal)(e.target.checked)}
/>
</label>
<button type="button" className="btn" onClick={resetFilters}>
{t.filters.reset}
</button>
@@ -234,7 +248,14 @@ export default function GerbilsPage() {
aria-label={g.name}
/>
<Link to={`/rennmaeuse/${g.id}`} className="gerbil-card">
<span className="gerbil-card__name">{g.name}</span>
<span className="gerbil-card__name">
{g.name}
{g.isResident === false && (
<span className="badge badge--external" title={t.externalTitle}>
{t.externalBadge}
</span>
)}
</span>
<span className={`badge badge--${g.status.toLowerCase()}`}>
{statusLabel(g.status)}
</span>

View File

@@ -49,6 +49,9 @@ export const de = {
empty: 'Keine Rennmäuse gefunden.',
countLabel: 'Tiere',
searchPlaceholder: 'Name suchen …',
// BESTAND-FILTER: Kennzeichnung externer (nicht zum eigenen Bestand gehörender) Tiere
externalBadge: 'Extern',
externalTitle: 'Externes Tier — gehört nicht zum eigenen Bestand, nur für den Stammbaum erfasst.',
// Filter-Beschriftungen
filters: {
title: 'Filter',
@@ -58,6 +61,9 @@ export const de = {
all: 'Alle',
reset: 'Filter zurücksetzen',
sortBy: 'Sortieren nach',
// BESTAND-FILTER: eigener Bestand (Clan) vs. externe Ahnen (nur Stammbaum)
showExternal: 'Externe Ahnen einblenden',
showExternalHint: 'Tiere aus fremden Zuchten, die nur für den Stammbaum erfasst sind.',
},
// Sortier-Optionen
sort: {