diff --git a/gerbil-manager-web/e2e/mock-data.ts b/gerbil-manager-web/e2e/mock-data.ts index f61fcb3..d80e457 100644 --- a/gerbil-manager-web/e2e/mock-data.ts +++ b/gerbil-manager-web/e2e/mock-data.ts @@ -108,6 +108,7 @@ function gerbil( notes: null, // BESTAND-FILTER: default = eigener Bestand; einzelne externe Ahnen unten gesetzt. isResident: true, + profilePhotoUrl: null, } } @@ -120,7 +121,7 @@ export function seedDb(): MockDb { originContactId: 'con-meier', originBreeder: 'Clan-Kleine-Chaoten', }, - { ...gerbil('fridolin', 'Fridolin', 'male', '2023-05-01', 'w-fridolin', 'cv-schwarz', 'aa CC DD EE GG PP spsp rere'), enclosureId: 'enc-gross', originBreeder: 'Zoohandlung Meier' }, + { ...gerbil('fridolin', 'Fridolin', 'male', '2023-05-01', 'w-fridolin', 'cv-schwarz', 'aa CC DD EE GG PP spsp rere'), enclosureId: 'enc-gross', originBreeder: 'Zoohandlung Meier', profilePhotoUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==' }, gerbil('luna', 'Luna', 'female', '2023-08-15', 'w-luna', 'cv-gold', 'AA CC DD EE GG pp spsp rere'), gerbil('balu', 'Balu', 'male', '2021-04-20', 'w-balu', 'cv-agouti'), gerbil('maja', 'Maja', 'female', '2021-06-11', null, 'cv-schwarz-schecke', 'aa CC DD EE GG PP Spsp rere'), diff --git a/gerbil-manager-web/e2e/stammbaum.spec.ts b/gerbil-manager-web/e2e/stammbaum.spec.ts index e9d6ecf..11e0ad7 100644 --- a/gerbil-manager-web/e2e/stammbaum.spec.ts +++ b/gerbil-manager-web/e2e/stammbaum.spec.ts @@ -154,3 +154,20 @@ test('Gencode-Chip zeigt Genotyp statt Farbname (STAMMBAUM-GENCODE)', async ({ p const fridolinCard = page.locator('.pedigree-card').filter({ hasText: 'Fridolin' }) await expect(fridolinCard.locator('.pedigree-chip')).toContainText('aa CC') }) + +test('Tier mit Foto zeigt Avatar-Bild, Tier ohne Foto zeigt Rennmaus-Icon (STAMMBAUM-AVATAR)', async ({ page }) => { + skipUnlessMock() + // Fridolin hat profilePhotoUrl (data URI) → sichtbar + // Krümel hat profilePhotoUrl=null → kein img, SVG-Icon sichtbar + await page.goto('/rennmaeuse/kruemel/stammbaum') + await expect(page.locator('.pedigree-card').first()).toBeVisible() + + // Fridolin ist Vater von Krümel → seine Karte enthält ein + const fridolinCard = page.locator('.pedigree-card').filter({ hasText: 'Fridolin' }) + await expect(fridolinCard.locator('img.pedigree-card__avatar')).toBeVisible() + + // Krümel (root) hat kein Foto → SVG-Icon im Foto-Slot + const rootCard = page.locator('.pedigree-card.pedigree-card--root') + await expect(rootCard.locator('img.pedigree-card__avatar')).not.toBeAttached() + await expect(rootCard.locator('svg[role="img"]')).toBeVisible() +}) diff --git a/gerbil-manager-web/src/api/types.ts b/gerbil-manager-web/src/api/types.ts index e9b75f9..6e6b976 100644 --- a/gerbil-manager-web/src/api/types.ts +++ b/gerbil-manager-web/src/api/types.ts @@ -56,6 +56,8 @@ export interface Gerbil { isResident?: boolean /** FORM-FIELDS-1: true=gehörlos, false=hörend, null=unbekannt. */ isDeaf?: boolean | null + /** STAMMBAUM-AVATAR: URL des Profilfotos (erstes Foto nach sortOrder); null = kein Foto. API-relativ oder absolut. */ + profilePhotoUrl?: string | null } /** Payload for POST /gerbils. */ diff --git a/gerbil-manager-web/src/pages/StammbaumPage.tsx b/gerbil-manager-web/src/pages/StammbaumPage.tsx index a450599..4612121 100644 --- a/gerbil-manager-web/src/pages/StammbaumPage.tsx +++ b/gerbil-manager-web/src/pages/StammbaumPage.tsx @@ -21,7 +21,7 @@ import { Link, useNavigate, useParams } from 'react-router-dom' import Tree from 'react-d3-tree' import type { CustomNodeElementProps, Point, RawNodeDatum } from 'react-d3-tree' import { de } from '../strings/de' -import { ApiError } from '../api/client' +import { API_BASE_URL, ApiError } from '../api/client' import { listLitters } from '../api/litters' import { listColorVarieties } from '../api/lookups' import { getInbreedingCoefficient } from '../api/pedigree' @@ -381,6 +381,14 @@ function PedigreeCard({ const g = node.gerbil const chip = farbschlag ? chipColorFor(farbschlag) : null const dob = g.dateOfBirth ? formatDate(g.dateOfBirth) : null + const [imgError, setImgError] = useState(false) + useEffect(() => setImgError(false), [g.id]) + const photoUrl = + !imgError && g.profilePhotoUrl + ? /^(https?:|data:|\/\/)/.test(g.profilePhotoUrl) + ? g.profilePhotoUrl + : `${API_BASE_URL}${g.profilePhotoUrl}` + : null return (
- {/* Foto-Platzhalter — echte Fotos kommen mit FEAT-6. */} -