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
280 lines
9.7 KiB
JavaScript
280 lines
9.7 KiB
JavaScript
/* ============================================================
|
||
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 =>
|
||
({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 };
|
||
|
||
})();
|