From b17706e7ba35686d619a997e208640d58b915cd9 Mon Sep 17 00:00:00 2001 From: rene Date: Wed, 13 May 2026 18:47:49 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Z=C3=BCchter-Profil=20Komplett-Redes?= =?UTF-8?q?ign=20=E2=80=94=20Hero,=20Hunde+Tests,=20W=C3=BCrfe,=20Gesundhe?= =?UTF-8?q?itsstatistik=20(SW=20by-v900)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 2 +- backend/routes/breeder.py | 83 +++++- backend/static/index.html | 8 +- backend/static/js/app.js | 2 +- backend/static/js/pages/breeder.js | 408 +++++++++++++++++++---------- backend/static/sw.js | 2 +- 6 files changed, 362 insertions(+), 143 deletions(-) diff --git a/backend/main.py b/backend/main.py index 9ab4a1e..8beb2ba 100644 --- a/backend/main.py +++ b/backend/main.py @@ -406,7 +406,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "899" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "900" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/breeder.py b/backend/routes/breeder.py index 22e6fde..2f3dac4 100644 --- a/backend/routes/breeder.py +++ b/backend/routes/breeder.py @@ -301,7 +301,7 @@ async def admin_reject_breeder(user_id: int, body: RejectBody, admin=Depends(req # ------------------------------------------------------------------ -# GET /api/breeder/profil/{zwingername} — öffentliches Profil +# GET /api/breeder/profil/{zwingername} — öffentliches Profil (angereichert) # ------------------------------------------------------------------ @router.get("/breeder/profil/{zwingername}") async def breeder_public_profile(zwingername: str): @@ -318,9 +318,84 @@ async def breeder_public_profile(zwingername: str): AND u.rolle IN ('breeder', 'admin') AND (u.breeder_status = 'approved' OR u.rolle = 'admin') """, (zwingername,)).fetchone() - if not row: - raise HTTPException(404, "Züchter nicht gefunden.") - return dict(row) + if not row: + raise HTTPException(404, "Züchter nicht gefunden.") + + breeder_id = row["id"] + result = dict(row) + + # Öffentliche Zuchthunde + ihre wichtigsten Gesundheitstests + Titel + hunde_rows = conn.execute(""" + SELECT id, name, rufname, geschlecht, geburtsdatum, farbe, zuchtbuchnummer, foto_url + FROM zucht_hunde + WHERE breeder_id=? AND is_public=1 AND (sterbedatum IS NULL OR sterbedatum='') + ORDER BY geschlecht, name + """, (breeder_id,)).fetchall() + + hunde = [] + for h in hunde_rows: + hund = dict(h) + # Gesundheitstests (nur öffentliche, nur HD/ED/Augen/Herz) + tests = conn.execute(""" + SELECT test_typ, ergebnis, test_name, untersuch_am + FROM dog_health_tests + WHERE hund_id=? AND is_public=1 + AND test_typ IN ('HD','ED','augen','herz','OCD','patella','ZTP') + ORDER BY test_typ, untersuch_am DESC + """, (h["id"],)).fetchall() + seen = set() + hund["health_tests"] = [] + for t in tests: + if t["test_typ"] not in seen: + seen.add(t["test_typ"]) + hund["health_tests"].append(dict(t)) + # Gentests (nur öffentliche, Zusammenfassung) + gentests = conn.execute(""" + SELECT COUNT(*) as total, + SUM(CASE WHEN ergebnis_klasse='clear' THEN 1 ELSE 0 END) as clear_cnt + FROM dog_genetic_tests WHERE hund_id=? AND is_public=1 + """, (h["id"],)).fetchone() + hund["gentests_total"] = gentests["total"] or 0 + hund["gentests_clear"] = gentests["clear_cnt"] or 0 + # Auszeichnungen (nur Zucht/Champion) + titles = conn.execute(""" + SELECT titel_name FROM dog_titles + WHERE hund_id=? AND titel_typ IN ('champion','zucht','ausstellung') + ORDER BY verliehen_am DESC LIMIT 3 + """, (h["id"],)).fetchall() + hund["titel"] = [t["titel_name"] for t in titles] + hunde.append(hund) + + result["hunde"] = hunde + + # Sichtbare Würfe + wuerfe = conn.execute(""" + SELECT id, vater_name, mutter_name, geburt_datum, erwartetes_datum, + status, welpen_gesamt, welpen_verfuegbar, preis_spanne, beschreibung + FROM litters + WHERE breeder_id=? AND sichtbar=1 AND status != 'abgeschlossen' + ORDER BY COALESCE(geburt_datum, erwartetes_datum) DESC + """, (breeder_id,)).fetchall() + result["wuerfe"] = [dict(w) for w in wuerfe] + + # Gesundheits-Statistik (aggregiert über alle öffentlichen Hunde) + hd_stats = conn.execute(""" + SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests + WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1) + AND test_typ='HD' AND is_public=1 + GROUP BY ergebnis + """, (breeder_id,)).fetchall() + result["hd_stats"] = [dict(r) for r in hd_stats] + + ed_stats = conn.execute(""" + SELECT ergebnis, COUNT(*) as cnt FROM dog_health_tests + WHERE hund_id IN (SELECT id FROM zucht_hunde WHERE breeder_id=? AND is_public=1) + AND test_typ='ED' AND is_public=1 + GROUP BY ergebnis + """, (breeder_id,)).fetchall() + result["ed_stats"] = [dict(r) for r in ed_stats] + + return result # ------------------------------------------------------------------ diff --git a/backend/static/index.html b/backend/static/index.html index 07cacba..d45b667 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -591,10 +591,10 @@ - - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 7b5af08..0219d30 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '899'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '900'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.1'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/breeder.js b/backend/static/js/pages/breeder.js index 5c50ec4..907469f 100644 --- a/backend/static/js/pages/breeder.js +++ b/backend/static/js/pages/breeder.js @@ -1,6 +1,5 @@ /* ============================================================ - BAN YARO — Öffentliches Züchter-Profil - Seiten-Modul: Zeigt das verifizierte Profil eines Züchters. + BAN YARO — Öffentliches Züchter-Profil (Visitenkarte) ============================================================ */ window.Page_breeder = (() => { @@ -8,7 +7,8 @@ window.Page_breeder = (() => { let _container = null; let _appState = null; - const _esc = s => UI.esc ? UI.esc(s) : String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + const _esc = s => UI.esc ? UI.esc(s) : String(s ?? '').replace(/[&<>"']/g, + c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); // ---------------------------------------------------------- // INIT @@ -17,7 +17,6 @@ window.Page_breeder = (() => { _container = container; _appState = appState; - // Zwingername aus params oder URL-Pfad (/breeder/vom-sonnenfeld) const zwingername = params?.zwingername || decodeURIComponent((window.location.pathname.split('/breeder/')[1] || '').replace(/\/$/, '')); @@ -27,8 +26,8 @@ window.Page_breeder = (() => { } container.innerHTML = ` -
- ${UI.skeleton(3)} +
+ ${UI.skeleton(5)}
` + : `` + } + ${p.website ? ` + ${UI.icon('arrow-square-out')} Website + ` : ''} +
` : ''} + - -
-
+
-
-
Rasse
-
${_esc(p.rasse_text || '–')}
-
+ + ${p.beschreibung ? ` +
+

${_esc(p.beschreibung)}

+
` : ''} -
-
Verein
-
${_esc(p.verein || '–')}
-
+ + ${p.hunde?.length ? ` +
+

+ ${UI.icon('dog')} Unsere Zuchthunde + ${p.hunde.length} Hunde +

+
+ ${p.hunde.map(h => _hundCard(h)).join('')} +
+
` : ''} -
-
VDH-Mitglied
-
- ${p.vdh_mitglied - ? `${UI.icon('check')} Ja` - : `Nein`} -
-
+ + ${p.wuerfe?.length ? ` +
+

+ ${UI.icon('baby')} Aktuelle Würfe +

+
+ ${p.wuerfe.map(w => _wurfCard(w)).join('')} +
+
` : ''} -
-
Stadt
-
${_esc(p.stadt || '–')}
-
+ + ${(p.hd_stats?.length || p.ed_stats?.length) ? ` +
+

+ ${UI.icon('heartbeat')} Gesundheits-Transparenz +

+
+ ${_statsSection('HD-Ergebnisse', p.hd_stats)} + ${p.hd_stats?.length && p.ed_stats?.length ? '
' : ''} + ${_statsSection('ED-Ergebnisse', p.ed_stats)} +
+
` : ''} + +
+

+ ${UI.icon('info')} Über den Züchter +

+
+ ${_dl('Züchter', p.zuechter_name)} + ${_dl('Rasse(n)', p.rasse_text)} + ${_dl('Verein', p.verein)} + ${_dl('VDH-Mitglied', p.vdh_mitglied ? '✓ Ja' : 'Nein')} + ${_dl('Stadt', p.stadt)} ${p.website ? `
-
Website
-
${websiteHtml}
+
Website
+
${_esc(p.website)}
` : ''} - - ${verifiedDate ? ` -
-
Verifiziert
-
${verifiedDate}
-
` : ''} - -
-
Züchter
-
${_esc(p.zuechter_name || '–')}
-
- + ${seit ? _dl('Züchter seit', seit) : ''}
- - ${beschreibungHtml} - - +
- - ${(() => { - if (!p.zuechter_user_id) return ''; - const isLoggedIn = !!_appState?.user; - const isOwnProfile = _appState?.user?.id === p.zuechter_user_id; - if (isOwnProfile) return ''; - if (isLoggedIn) { - return ``; - } - return ``; - })()} +
`; -
- `; + // Events + body.querySelector('.breeder-chat-btn')?.addEventListener('click', () => _contactBreeder(p.zuechter_user_id)); + body.querySelector('.breeder-login-btn')?.addEventListener('click', () => App.navigate('settings')); - _container.querySelector('.breeder-chat-btn')?.addEventListener('click', () => { - _contactBreeder(p.zuechter_user_id); - }); - _container.querySelector('.breeder-login-btn')?.addEventListener('click', () => { - App.navigate('settings'); - }); - - // Öffentliche Fotos nachladen _loadBreederPhotos(p.id); } + // ---------------------------------------------------------- + // Hund-Karte + // ---------------------------------------------------------- + function _hundCard(h) { + const alter = h.geburtsdatum + ? Math.floor((Date.now() - new Date(h.geburtsdatum)) / 31557600000) + : null; + const gIcon = h.geschlecht === 'maennlich' ? UI.icon('gender-male') : UI.icon('gender-female'); + + const hdTest = h.health_tests?.find(t => t.test_typ === 'HD'); + const edTest = h.health_tests?.find(t => t.test_typ === 'ED'); + const augeTest = h.health_tests?.find(t => t.test_typ === 'augen'); + + const testPills = [ + hdTest ? `HD ${_esc(hdTest.ergebnis)}` : '', + edTest ? `ED ${_esc(edTest.ergebnis)}` : '', + augeTest ? `Augen ✓` : '', + ].filter(Boolean).join(''); + + const titlePills = (h.titel || []).map(t => + `${_esc(t)}` + ).join(''); + + const genBadge = h.gentests_total > 0 + ? ` + ${h.gentests_clear}/${h.gentests_total} Gentests frei + ` + : ''; + + return ` +
+
+ ${gIcon} + ${_esc(h.name)} + ${h.rufname ? `"${_esc(h.rufname)}"` : ''} + ${alter !== null ? `${alter} J.` : ''} +
+ ${h.farbe ? `

${_esc(h.farbe)}

` : ''} + ${testPills ? `
${testPills}
` : ''} + ${titlePills ? `
${titlePills}
` : ''} + ${genBadge} +
`; + } + + function _testPillStyle(ergebnis, typ) { + const e = (ergebnis || '').toUpperCase(); + let bg = '#6b72801a', color = '#6b7280', border = '#6b728040'; + if (typ === 'HD') { + if (['A','A1','A2'].includes(e)) { bg='#16a34a1a';color='#16a34a';border='#16a34a40'; } + else if (e === 'B' || e === 'B1' || e === 'B2') { bg='#86efac1a';color='#15803d';border='#86efac40'; } + else if (e === 'C') { bg='#eab3081a';color='#a16207';border='#eab30840'; } + else if (e === 'D' || e === 'E') { bg='#ef44441a';color='#dc2626';border='#ef444440'; } + } else if (typ === 'ED') { + if (e === '0' || e === 'ED 0') { bg='#16a34a1a';color='#16a34a';border='#16a34a40'; } + else if (e === '1') { bg='#eab3081a';color='#a16207';border='#eab30840'; } + else if (e === '2' || e === '3') { bg='#ef44441a';color='#dc2626';border='#ef444440'; } + } else if (typ === 'augen' || ergebnis === 'clear') { + bg='#16a34a1a';color='#16a34a';border='#16a34a40'; + } + return `background:${bg};color:${color};border:1px solid ${border};border-radius:999px;padding:1px 8px;font-size:11px;font-weight:600`; + } + + // ---------------------------------------------------------- + // Wurf-Karte + // ---------------------------------------------------------- + const _STATUS_LABEL = { geplant: 'Geplant', geboren: 'Geboren', verfuegbar: 'Verfügbar', abgeschlossen: 'Abgeschlossen' }; + const _STATUS_COLOR = { geplant: '#6b7280', geboren: '#3b82f6', verfuegbar: '#16a34a', abgeschlossen: '#9ca3af' }; + + function _wurfCard(w) { + const eltern = [w.vater_name, w.mutter_name].filter(Boolean).join(' × ') || '—'; + const datum = w.geburt_datum + ? `Geburt: ${_fmtDate(w.geburt_datum)}` + : w.erwartetes_datum ? `Erwartet: ${_fmtDate(w.erwartetes_datum)}` : ''; + const sc = _STATUS_COLOR[w.status] || '#6b7280'; + const sl = _STATUS_LABEL[w.status] || w.status; + return ` +
+
+ ${_esc(eltern)} + ${sl} +
+
+ ${datum ? `${UI.icon('calendar-dots')} ${_esc(datum)}` : ''} + ${w.welpen_gesamt ? `${UI.icon('dog')} ${w.welpen_verfuegbar ?? '?'}/${w.welpen_gesamt} verfügbar` : ''} + ${w.preis_spanne ? `${UI.icon('currency-eur')} ${_esc(w.preis_spanne)}` : ''} +
+ ${w.beschreibung ? `

${_esc(w.beschreibung)}

` : ''} +
`; + } + + // ---------------------------------------------------------- + // Statistik-Sektion + // ---------------------------------------------------------- + function _statsSection(label, stats) { + if (!stats?.length) return ''; + const total = stats.reduce((s, r) => s + r.cnt, 0); + return ` +
+

${_esc(label)}

+
+ ${stats.map(r => ` +
+ ${_esc(r.ergebnis || '—')} + ${r.cnt}× + +
`).join('')} +
+
`; + } + + // ---------------------------------------------------------- + // Hilfsfunktionen + // ---------------------------------------------------------- + function _dl(label, value) { + if (!value) return ''; + return `
+
${_esc(label)}
+
${_esc(String(value))}
+
`; + } + + function _fmtDate(iso) { + if (!iso) return '—'; + const [y,m,d] = iso.slice(0,10).split('-'); + return `${d}.${m}.${y}`; + } + async function _loadBreederPhotos(breederId) { const section = document.getElementById('breeder-photos-section'); if (!section) return; try { const photos = await API.breederPhotos.list('breeder', breederId); - if (!photos || !photos.length) return; - + if (!photos?.length) return; section.innerHTML = ` -
-

+
+

${UI.icon('images')} Fotos -

-
- ${photos.map(ph => { - const thumb = ph.thumbnail_url || ph.url || ''; - return ` - - ${_esc(ph.caption || '')} - `; - }).join('')} +

+
+ ${photos.map(ph => ` + + ${_esc(ph.caption||'')} + `).join('')}
`; - } catch (_) { - // Fotos sind nicht kritisch — bei Fehler still ignorieren - } + } catch (_) {} } - async function _contactBreeder(breederId) { - if (!_appState?.user) { - App.navigate('settings'); - return; - } + async function _contactBreeder(userId) { + if (!_appState?.user) { App.navigate('settings'); return; } try { - await API.chat.start(breederId); + await API.chat.start(userId); App.navigate('chat'); - } catch (e) { - UI.toast.error(e.message || 'Chat konnte nicht geöffnet werden.'); - } + } catch (e) { UI.toast.error(e.message || 'Chat konnte nicht geöffnet werden.'); } } function refresh() {} function onDogChange() {} - function destroy() { - document.getElementById('breeder-back-fab')?.remove(); - } + function destroy() { document.getElementById('breeder-back-fab')?.remove(); } return { init, refresh, onDogChange, destroy }; diff --git a/backend/static/sw.js b/backend/static/sw.js index e90b85b..42fb8dc 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v899'; +const CACHE_VERSION = 'by-v900'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache