banyaro/backend/static/js/pages/wurfboerse.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

280 lines
9.7 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 — Wurfbörse
Öffentliche Wurfankündigungen aller Züchter
============================================================ */
window.Page_wurfboerse = (() => {
let _container = null;
let _appState = null;
let _data = [];
// ----------------------------------------------------------
// Hilfsfunktionen
// ----------------------------------------------------------
function _esc(s) {
return UI.escape ? UI.escape(s || '') : (s || '').replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function _fmtDate(iso) {
if (!iso) return '—';
const d = new Date(iso + 'T12:00:00');
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function _statusBadge(status) {
const map = {
geplant: { label: 'Geplant', cls: 'wb-badge--geplant' },
geboren: { label: 'Geboren', cls: 'wb-badge--geboren' },
verfuegbar: { label: 'Verfügbar', cls: 'wb-badge--verfuegbar' },
abgeschlossen: { label: 'Abgeschlossen', cls: 'wb-badge--abgeschlossen' },
};
const s = map[status] || { label: status, cls: 'wb-badge--geplant' };
return `<span class="wb-badge ${s.cls}">${s.label}</span>`;
}
function _truncate(text, max) {
if (!text) return '';
return text.length > max ? text.slice(0, max).trimEnd() + '…' : text;
}
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
async function init(container, appState) {
_container = container;
_appState = appState;
_render();
await _loadData();
}
function refresh() { _loadData(); }
function onDogChange() {}
// ----------------------------------------------------------
// Grundstruktur rendern
// ----------------------------------------------------------
function _render() {
_container.innerHTML = `
<div class="wb-layout">
<!-- Filter-Leiste -->
<div class="wb-filter-bar">
<div class="wb-filter-fields">
<input
class="form-control wb-filter-rasse"
id="wb-filter-rasse"
type="text"
placeholder="Rasse suchen…"
autocomplete="off"
>
<select class="form-control wb-filter-status" id="wb-filter-status">
<option value="">Alle Status</option>
<option value="geplant">Geplant</option>
<option value="verfuegbar">Verfügbar</option>
<option value="geboren">Geboren</option>
</select>
</div>
<button class="btn btn-primary wb-filter-btn" id="wb-search-btn">
${UI.icon('magnifying-glass')} Suchen
</button>
</div>
<!-- Ergebnisliste -->
<div id="wb-list">
<p style="color:var(--c-text-secondary);text-align:center;padding:var(--space-8)">Lädt…</p>
</div>
</div>
`;
// Suchen-Button
document.getElementById('wb-search-btn').addEventListener('click', () => _loadData());
// Enter im Rasse-Feld
document.getElementById('wb-filter-rasse').addEventListener('keydown', e => {
if (e.key === 'Enter') _loadData();
});
}
// ----------------------------------------------------------
// Daten laden
// ----------------------------------------------------------
async function _loadData() {
const rasseEl = document.getElementById('wb-filter-rasse');
const statusEl = document.getElementById('wb-filter-status');
const params = {};
if (rasseEl?.value.trim()) params.rasse = rasseEl.value.trim();
if (statusEl?.value) params.status = statusEl.value;
try {
_data = await API.litters.public(params);
_renderList();
} catch (err) {
UI.toast.error(err.message || 'Fehler beim Laden der Wurfbörse.');
_renderEmpty('Fehler beim Laden', 'Bitte später erneut versuchen.');
}
}
// ----------------------------------------------------------
// Liste rendern
// ----------------------------------------------------------
function _renderList() {
const el = document.getElementById('wb-list');
if (!el) return;
if (!_data.length) {
_renderEmpty('Keine Würfe gefunden', 'Für die gewählten Filter gibt es aktuell keine Wurfankündigungen.');
return;
}
el.innerHTML = `<div class="wb-cards">${_data.map(b => _cardHTML(b)).join('')}</div>`;
el.querySelectorAll('.wb-profile-btn').forEach(btn => {
btn.addEventListener('click', () => {
const zwingername = btn.dataset.zwingername;
App.navigate('breeder', true, { zwingername });
});
});
el.querySelectorAll('.wb-chat-btn').forEach(btn => {
btn.addEventListener('click', () => {
const breederId = parseInt(btn.dataset.breederUserId, 10);
_contactBreeder(breederId);
});
});
el.querySelectorAll('.wb-login-btn').forEach(btn => {
btn.addEventListener('click', () => App.navigate('settings'));
});
}
function _renderEmpty(title, text) {
const el = document.getElementById('wb-list');
if (!el) return;
el.innerHTML = `
<div style="text-align:center;padding:var(--space-10) var(--space-4)">
<div style="font-size:3rem;margin-bottom:var(--space-3)">${UI.icon('dog')}</div>
<h3 style="margin:0 0 var(--space-2)">${_esc(title)}</h3>
<p style="color:var(--c-text-secondary);margin:0">${_esc(text)}</p>
</div>`;
}
// ----------------------------------------------------------
// Card HTML
// ----------------------------------------------------------
function _cardHTML(b) {
// Züchter-Kopfzeile
const zuechterName = b.zuechter_name || b.zwingername || '—';
const zwingername = b.zwingername ? ` (${_esc(b.zwingername)})` : '';
const stadtLine = b.stadt ? ` · ${_esc(b.stadt)}` : '';
// Elterntiere
const elternParts = [];
if (b.vater_name) elternParts.push(_esc(b.vater_name));
if (b.mutter_name) elternParts.push(_esc(b.mutter_name));
const elternLine = elternParts.length === 2
? `<div class="wb-card-eltern">${UI.icon('gender-male')} ${elternParts[0]} × ${UI.icon('gender-female')} ${elternParts[1]}</div>`
: elternParts.length === 1
? `<div class="wb-card-eltern">${elternParts[0]}</div>`
: '';
// Datum
let datumLine = '';
if (b.geburt_datum) {
datumLine = `<div class="wb-card-datum">${UI.icon('calendar-dots')} Geboren: ${_fmtDate(b.geburt_datum)}</div>`;
} else if (b.erwartetes_datum) {
datumLine = `<div class="wb-card-datum">${UI.icon('calendar-dots')} Erwartet: ${_fmtDate(b.erwartetes_datum)}</div>`;
}
// Welpen-Verfügbarkeit
let welpenLine = '';
if (b.welpen_gesamt != null || b.welpen_verfuegbar != null) {
const gesamt = b.welpen_gesamt != null ? b.welpen_gesamt : '?';
const verfuegb = b.welpen_verfuegbar != null ? b.welpen_verfuegbar : '?';
welpenLine = `<div class="wb-card-welpen">${UI.icon('paw-print')} Welpen verfügbar: ${_esc(String(verfuegb))} von ${_esc(String(gesamt))}</div>`;
}
// Preis
const preisLine = b.preis_spanne
? `<div class="wb-card-preis">${UI.icon('currency-eur')} Preis: ${_esc(b.preis_spanne)} €</div>`
: '';
// Gesundheitstests
const gesundheitLine = b.gesundheitstests
? `<div class="wb-card-gesundheit">${UI.icon('heart')} ${_esc(b.gesundheitstests)}</div>`
: '';
// Beschreibung (max. 150 Zeichen)
const beschreibungLine = b.beschreibung
? `<div class="wb-card-beschreibung">${_esc(_truncate(b.beschreibung, 150))}</div>`
: '';
return `
<div class="wb-card">
<div class="wb-card-header">
<div class="wb-card-zuechter">
${_esc(zuechterName)}${zwingername}${stadtLine}
</div>
${_statusBadge(b.status)}
</div>
${b.rasse_text ? `<div class="wb-card-rasse">${UI.icon('dog')} ${_esc(b.rasse_text)}</div>` : ''}
<div class="wb-card-details">
${elternLine}
${datumLine}
${welpenLine}
${preisLine}
${gesundheitLine}
${beschreibungLine}
</div>
<div class="wb-card-footer">
<button
class="btn btn-secondary btn-sm wb-profile-btn"
data-zwingername="${_esc(b.zwingername || '')}"
>
${UI.icon('user')} Profil ansehen
</button>
${(() => {
const isLoggedIn = !!_appState?.user;
const isOwnProfile = _appState?.user?.id === b.breeder_user_id;
if (isOwnProfile) return '';
if (isLoggedIn) {
return `<button
class="btn btn-primary btn-sm wb-chat-btn"
data-breeder-user-id="${b.breeder_user_id || ''}"
>
${UI.icon('chat-circle')} Nachricht senden
</button>`;
}
return `<button class="btn btn-primary btn-sm wb-login-btn">
${UI.icon('sign-in')} Anmelden um zu schreiben
</button>`;
})()}
</div>
</div>`;
}
// ----------------------------------------------------------
// Züchter per Chat kontaktieren
// ----------------------------------------------------------
async function _contactBreeder(breederId) {
if (!_appState?.user) {
App.navigate('settings');
return;
}
try {
const conv = await API.chat.start(breederId);
App.navigate('chat');
} catch (e) {
UI.toast.error(e.message || 'Chat konnte nicht geöffnet werden.');
}
}
return { init, refresh, onDogChange };
})();