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
This commit is contained in:
parent
58cb2b4ad3
commit
91340be5a3
24 changed files with 6660 additions and 27 deletions
280
backend/static/js/pages/wurfboerse.js
Normal file
280
backend/static/js/pages/wurfboerse.js
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
/* ============================================================
|
||||
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 };
|
||||
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue