Merge feature/animal-litters (ANIMAL-LITTERS): 'Wuerfe als Elternteil' auf Tier-Detailseite
GerbilDetailPage: Sektion listet Wuerfe wo fatherId|motherId==Tier (Gridify OR-Filter),
je Link /wuerfe/{id} + Vater/Mutter-Badge + Datum + totalBorn; Leer-Zustand. Rueckweg Tier→Wuerfe.
Frontend-only. vitest 104, e2e 160.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -172,3 +172,27 @@ test('Tier bearbeiten — isDeaf Tristate round-trip (FORM-FIELDS-1)', async ({
|
|||||||
await page.goto('/rennmaeuse/kruemel/bearbeiten')
|
await page.goto('/rennmaeuse/kruemel/bearbeiten')
|
||||||
await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('true')
|
await expect(page.getByLabel(t.fields.isDeaf)).toHaveValue('true')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Detailseite: Würfe als Elternteil zeigt verlinkten Wurf (ANIMAL-LITTERS)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
// Fridolin ist fatherId von 'w-kruemel' (Wurf K)
|
||||||
|
await page.goto('/rennmaeuse/fridolin')
|
||||||
|
await expect(page.getByRole('heading', { name: 'Fridolin' })).toBeVisible()
|
||||||
|
await expect(page.getByText(t.detail.parentLittersTitle)).toBeVisible()
|
||||||
|
// Wurf K als Link sichtbar
|
||||||
|
const litterLink = page.getByRole('link', { name: /Wurf K/ })
|
||||||
|
await expect(litterLink).toBeVisible()
|
||||||
|
// Vater-Badge sichtbar
|
||||||
|
await expect(page.getByText(t.detail.parentLittersRoleVater)).toBeVisible()
|
||||||
|
// Navigation zum Wurf funktioniert
|
||||||
|
await litterLink.first().click()
|
||||||
|
await expect(page.getByRole('heading', { name: 'Wurf K' })).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Detailseite: Keine Würfe als Elternteil zeigt Leer-Zustand (ANIMAL-LITTERS)', async ({ page }) => {
|
||||||
|
skipUnlessMock()
|
||||||
|
// Krümel hat keine Würfe als Elternteil (nur als Kind von w-kruemel)
|
||||||
|
await page.goto('/rennmaeuse/kruemel')
|
||||||
|
await expect(page.getByText(t.detail.parentLittersTitle)).toBeVisible()
|
||||||
|
await expect(page.getByText(t.detail.parentLittersEmpty)).toBeVisible()
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMemo, useState, type ReactNode } from 'react'
|
|||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { de } from '../strings/de'
|
import { de } from '../strings/de'
|
||||||
import { getGerbil, updateGerbil } from '../api/gerbils'
|
import { getGerbil, updateGerbil } from '../api/gerbils'
|
||||||
|
import { listLitters as listLittersPaged } from '../api/litters'
|
||||||
import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups'
|
import { listColorVarieties, listContacts, listEnclosures, listLitters } from '../api/lookups'
|
||||||
import { useApi, useMutation } from '../hooks/useApi'
|
import { useApi, useMutation } from '../hooks/useApi'
|
||||||
import { formatDate, genderLabel, statusLabel } from '../format/labels'
|
import { formatDate, genderLabel, statusLabel } from '../format/labels'
|
||||||
@@ -55,6 +56,10 @@ export default function GerbilDetailPage() {
|
|||||||
const enclosures = useApi(() => listEnclosures(), [])
|
const enclosures = useApi(() => listEnclosures(), [])
|
||||||
const contacts = useApi(() => listContacts(), [])
|
const contacts = useApi(() => listContacts(), [])
|
||||||
const litters = useApi(() => listLitters(), [])
|
const litters = useApi(() => listLitters(), [])
|
||||||
|
const parentLitters = useApi(
|
||||||
|
() => listLittersPaged({ filter: `fatherId=${id}|motherId=${id}`, orderBy: 'date desc', pageSize: 50 }),
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
|
||||||
const colorName = useMemo(
|
const colorName = useMemo(
|
||||||
() => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])),
|
() => new Map((colorVarieties.data ?? []).map((c) => [c.id, c.name])),
|
||||||
@@ -237,6 +242,32 @@ export default function GerbilDetailPage() {
|
|||||||
{saveCharacter.error && <span className="error-text">{saveCharacter.error}</span>}
|
{saveCharacter.error && <span className="error-text">{saveCharacter.error}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>{t.detail.parentLittersTitle}</h3>
|
||||||
|
{parentLitters.loading && <p className="muted">{de.common.loading}</p>}
|
||||||
|
{!parentLitters.loading && (parentLitters.data?.items.length ?? 0) === 0 && (
|
||||||
|
<p className="muted">{t.detail.parentLittersEmpty}</p>
|
||||||
|
)}
|
||||||
|
{(parentLitters.data?.items.length ?? 0) > 0 && (
|
||||||
|
<ul className="card-list">
|
||||||
|
{(parentLitters.data?.items ?? []).map((litter) => (
|
||||||
|
<li key={litter.id}>
|
||||||
|
<Link to={`/wuerfe/${litter.id}`} className="gerbil-card">
|
||||||
|
<span className="gerbil-card__name">{litter.name}</span>
|
||||||
|
<span className="badge badge--active">
|
||||||
|
{litter.fatherId === id ? t.detail.parentLittersRoleVater : t.detail.parentLittersRoleMutter}
|
||||||
|
</span>
|
||||||
|
<span className="gerbil-card__meta">{formatDate(litter.date)}</span>
|
||||||
|
{litter.totalBorn !== null && (
|
||||||
|
<span className="gerbil-card__meta">
|
||||||
|
{litter.totalBorn} {de.pages.litters.countLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
<h3 className="visually-hidden">{t.detail.moreData}</h3>
|
<h3 className="visually-hidden">{t.detail.moreData}</h3>
|
||||||
<div className="tabs" role="tablist">
|
<div className="tabs" role="tablist">
|
||||||
{(['photos', 'health', 'weight'] as const).map((key) => (
|
{(['photos', 'health', 'weight'] as const).map((key) => (
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ export const de = {
|
|||||||
tabPlaceholder: 'Dieser Bereich entsteht in einem späteren Schritt.',
|
tabPlaceholder: 'Dieser Bereich entsteht in einem späteren Schritt.',
|
||||||
notFound: 'Diese Rennmaus wurde nicht gefunden.',
|
notFound: 'Diese Rennmaus wurde nicht gefunden.',
|
||||||
moreData: 'Weitere Daten',
|
moreData: 'Weitere Daten',
|
||||||
|
// ANIMAL-LITTERS: Würfe als Elternteil
|
||||||
|
parentLittersTitle: 'Würfe als Elternteil',
|
||||||
|
parentLittersEmpty: 'Keine Würfe als Elternteil erfasst.',
|
||||||
|
parentLittersRoleVater: 'Vater',
|
||||||
|
parentLittersRoleMutter: 'Mutter',
|
||||||
},
|
},
|
||||||
// Formular (anlegen/bearbeiten)
|
// Formular (anlegen/bearbeiten)
|
||||||
form: {
|
form: {
|
||||||
|
|||||||
Reference in New Issue
Block a user