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:
rene 2026-04-28 18:25:21 +02:00
parent 58cb2b4ad3
commit 91340be5a3
24 changed files with 6660 additions and 27 deletions

View file

@ -1011,6 +1011,14 @@ window.Page_health = (() => {
if (e.datum) rows.push(['Datum', UI.time.format(e.datum + 'T00:00:00')]);
if (e.typ === 'laeufigkeit' && e.wert) rows.push(['Dauer', `${e.wert} Tage`]);
if (e.naechstes) rows.push([e.typ === 'laeufigkeit' ? 'Nächste erwartet' : 'Nächstes', UI.time.format(e.naechstes + 'T00:00:00')]);
if (e.typ === 'laeufigkeit' && e.deckdatum) rows.push(['Deckdatum', UI.time.format(e.deckdatum + 'T00:00:00')]);
if (e.typ === 'laeufigkeit' && e.wurftermin) {
const wurfDate = new Date(e.wurftermin + 'T00:00:00');
const today = new Date(); today.setHours(0,0,0,0);
const diffDays = Math.round((wurfDate - today) / 86400000);
const zukunft = diffDays > 0 ? ` <span style="color:var(--c-primary);font-weight:600">in ${diffDays} Tagen</span>` : '';
rows.push(['Wurftermin', UI.time.format(e.wurftermin + 'T00:00:00') + zukunft]);
}
if (e.tierarzt_id) {
const praxis = _praxen.find(p => p.id === e.tierarzt_id);
if (praxis) {
@ -1478,6 +1486,27 @@ window.Page_health = (() => {
<input class="form-control" type="date" name="naechstes"
value="${entry?.naechstes || ''}" id="laeufi-naechstes">
</div>
${['breeder', 'admin'].includes(_appState.user?.rolle) ? `
<div class="form-group" id="laeufi-zuechter-fields" style="margin-top:var(--space-4);
padding-top:var(--space-4);border-top:1px solid var(--c-border)">
<div style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary);
text-transform:uppercase;letter-spacing:0.05em;margin-bottom:var(--space-3)">
Zucht (optional)
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
<div class="form-group">
<label class="form-label">Deckdatum</label>
<input class="form-control" type="date" name="deckdatum"
value="${entry?.deckdatum || ''}" id="laeufi-deckdatum">
</div>
<div class="form-group">
<label class="form-label">Wurftermin (63 Tage nach Deckung)</label>
<input class="form-control" type="date" name="wurftermin"
value="${entry?.wurftermin || ''}" id="laeufi-wurftermin" readonly
style="background:var(--c-surface-2)">
</div>
</div>
</div>` : ''}
<script>
(function() {
const datum = document.querySelector('[name="datum"]');
@ -1495,6 +1524,16 @@ window.Page_health = (() => {
datum?.addEventListener('change', updateNext);
interval?.addEventListener('change', updateNext);
if (!naechstes?.value) updateNext();
const deckdatum = document.getElementById('laeufi-deckdatum');
const wurftermin = document.getElementById('laeufi-wurftermin');
deckdatum?.addEventListener('change', e => {
const deckDate = new Date(e.target.value);
if (!isNaN(deckDate)) {
deckDate.setDate(deckDate.getDate() + 63);
wurftermin.value = deckDate.toISOString().split('T')[0];
}
});
})();
</script>
`;
@ -1524,6 +1563,8 @@ window.Page_health = (() => {
}
if (typ === 'laeufigkeit') {
p.bezeichnung = p.bezeichnung || 'Läufigkeit';
p.deckdatum = fd.deckdatum || null;
p.wurftermin = fd.wurftermin || null;
}
if (fd.kosten) p.kosten = parseFloat(fd.kosten.toString().replace(',', '.'));
if (fd.tierarzt_id) {