banyaro/backend/static/js/pages/breeder.js
rene 91340be5a3 Feature: Vollständige Züchter-Rolle — Antrag, Würfe, Stammbaum, Genetik
Basis-Features (Schritte 1–11):
- Züchter-Antrag mit Dokument-Upload, Admin-Prüfung, E-Mail-Benachrichtigungen
- Öffentliches Züchter-Profil + Karten-Marker (lila, certificate-Icon)
- Wurfverwaltung: Würfe, Welpen, Gewichtsverlauf, Foto-System
- Wurfbörse (öffentlich) mit Filtersuche nach Rasse/Status
- Läufigkeits-Tracker: Deckdatum + Wurftermin (+63 Tage, nur für Züchter)
- Interessenten-Chat: Kontakt-Button in Wurfbörse und Züchter-Profil
- Sidebar-Einträge: Zuchtkartei + Wurfverwaltung für Züchter/Admin

Stammbaum & Genetik (Schritte 1–8):
- Zuchtkartei: Hunde-Stammdaten mit Vater/Mutter-Verknüpfung
- Stammbaum-Visualisierung: 4 Generationen, horizontales CSS-Grid
- Gesundheitstests (HD, ED, OCD, Augen…) mit farbigen Ergebnis-Badges
- Genetische Tests (MDR1, PRA, DM…): clear/carrier/affected
- Titel & Auszeichnungen (CAC, CACIB, IPO…)
- Probeverpaarung: IK-Berechnung nach Wright + Ampel-Bewertung
- Teilen-Link für öffentliche Hunde-Profile
- Kaufvertrag: druckbares HTML-Dokument pro Welpe

Technisch: 4 neue Route-Dateien, 5 neue Page-Module, 11 neue DB-Tabellen,
icons shield-check + certificate + tree-structure im Sprite — SW by-v465, APP_VER 444
2026-04-28 18:25:21 +02:00

210 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
BAN YARO — Öffentliches Züchter-Profil
Seiten-Modul: Zeigt das verifizierte Profil eines Züchters.
============================================================ */
window.Page_breeder = (() => {
let _container = null;
let _appState = null;
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState, params = {}) {
_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(/\/$/, ''));
if (!zwingername) {
container.innerHTML = '<div style="padding:var(--space-6)">Kein Zwingername angegeben.</div>';
return;
}
container.innerHTML = '<div style="padding:var(--space-6);text-align:center">Lade…</div>';
try {
const p = await API.breeder.profile(zwingername);
_render(p);
} catch (e) {
container.innerHTML = `<div style="padding:var(--space-6)">${_esc(e.message || 'Züchter nicht gefunden.')}</div>`;
}
}
// ----------------------------------------------------------
// RENDER
// ----------------------------------------------------------
function _render(p) {
const verifiedDate = p.verified_at
? new Date(p.verified_at).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })
: null;
const websiteHtml = p.website
? `<a href="${_esc(p.website)}" target="_blank" rel="noopener noreferrer"
style="color:var(--c-primary);text-decoration:none;word-break:break-all">
${UI.icon('arrow-square-out')} ${_esc(p.website)}
</a>`
: '';
const beschreibungHtml = p.beschreibung
? `<div class="card" style="margin-bottom:var(--space-3)">
<p style="margin:0;white-space:pre-line;color:var(--c-text-secondary)">${_esc(p.beschreibung)}</p>
</div>`
: '';
_container.innerHTML = `
<div style="padding:var(--space-4)">
<!-- Header-Card -->
<div class="card" style="margin-bottom:var(--space-3)">
<div style="display:flex;align-items:flex-start;gap:var(--space-3);flex-wrap:wrap">
<div style="flex:1;min-width:0">
<h2 style="margin:0 0 var(--space-1);font-size:var(--text-xl);word-break:break-word">
${UI.icon('certificate')} ${_esc(p.zwingername)}
</h2>
<span class="badge badge-primary" style="background:var(--c-success,#22C55E);color:#fff;font-size:var(--text-xs)">
${UI.icon('seal-check')} Verifizierter Züchter
</span>
</div>
</div>
</div>
<!-- Details-Card -->
<div class="card" style="margin-bottom:var(--space-3)">
<dl style="margin:0;display:flex;flex-direction:column;gap:var(--space-3)">
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Rasse</dt>
<dd style="margin:0;font-weight:600">${_esc(p.rasse_text || '')}</dd>
</div>
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Verein</dt>
<dd style="margin:0">${_esc(p.verein || '')}</dd>
</div>
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">VDH-Mitglied</dt>
<dd style="margin:0">
${p.vdh_mitglied
? `<span class="badge badge-primary">${UI.icon('check')} Ja</span>`
: `<span style="color:var(--c-text-secondary)">Nein</span>`}
</dd>
</div>
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Stadt</dt>
<dd style="margin:0">${_esc(p.stadt || '')}</dd>
</div>
${p.website ? `
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Website</dt>
<dd style="margin:0">${websiteHtml}</dd>
</div>` : ''}
${verifiedDate ? `
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Verifiziert</dt>
<dd style="margin:0;color:var(--c-text-secondary);font-size:var(--text-sm)">${verifiedDate}</dd>
</div>` : ''}
<div style="display:flex;gap:var(--space-2);align-items:baseline">
<dt style="color:var(--c-text-secondary);min-width:100px;font-size:var(--text-sm);flex-shrink:0">Züchter</dt>
<dd style="margin:0">${_esc(p.zuechter_name || '')}</dd>
</div>
</dl>
</div>
<!-- Beschreibung -->
${beschreibungHtml}
<!-- Fotos (werden asynchron nachgeladen) -->
<div id="breeder-photos-section"></div>
<!-- Kontakt-Button -->
${(() => {
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 `<button class="btn btn-primary breeder-chat-btn" style="width:100%">
${UI.icon('chat-circle')} Nachricht senden
</button>`;
}
return `<button class="btn btn-primary breeder-login-btn" style="width:100%">
${UI.icon('sign-in')} Anmelden um zu schreiben
</button>`;
})()}
</div>
`;
_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);
}
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;
section.innerHTML = `
<div class="card" style="margin-bottom:var(--space-3)">
<h3 style="margin:0 0 var(--space-3);font-size:var(--text-md);font-weight:var(--weight-semibold)">
${UI.icon('images')} Fotos
</h3>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));gap:var(--space-2)">
${photos.map(ph => {
const thumb = ph.thumbnail_url || ph.url || '';
return `
<a href="${_esc(ph.url || '')}" target="_blank" rel="noopener noreferrer"
style="display:block;border-radius:var(--radius-md);overflow:hidden;
border:1px solid var(--c-border);aspect-ratio:1">
<img src="${_esc(thumb)}"
alt="${_esc(ph.caption || '')}"
loading="lazy"
style="width:100%;height:100%;object-fit:cover;display:block"
onerror="this.parentElement.style.display='none'">
</a>`;
}).join('')}
</div>
</div>`;
} catch (_) {
// Fotos sind nicht kritisch — bei Fehler still ignorieren
}
}
async function _contactBreeder(breederId) {
if (!_appState?.user) {
App.navigate('settings');
return;
}
try {
await API.chat.start(breederId);
App.navigate('chat');
} catch (e) {
UI.toast.error(e.message || 'Chat konnte nicht geöffnet werden.');
}
}
function refresh() {}
function onDogChange() {}
return { init, refresh, onDogChange };
})();