banyaro/promotion/banyaro_zuechterolle_ausarbeitung.md
rene 58cb2b4ad3 Dateien nach „promotion“ hochladen
erweiterte Züchterfunktionen, Stammbaum Genetik
2026-04-28 08:32:31 +02:00

1132 lines
36 KiB
Markdown
Raw Permalink 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.

# Züchter-Rolle — Banyaro.app
## Vollständige Ausarbeitung zur Umsetzung
---
## 1. Übersicht
### Rollen-Modell
| Rolle | Beschreibung |
|---|---|
| `user` | Normaler Hundebesitzer (bestehend) |
| `breeder` | Verifizierter Züchter (neu) |
| `admin` | René — Plattform-Administration (bestehend) |
Die Rolle `breeder` erweitert `user` — ein Züchter hat alle normalen Nutzerrechte plus Züchter-spezifische Features.
---
## 2. Datenbankstruktur
### Tabelle: `users` (Erweiterung)
```sql
ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'user';
-- Werte: 'user', 'breeder', 'admin'
ALTER TABLE users ADD COLUMN breeder_status VARCHAR(20) DEFAULT NULL;
-- Werte: NULL (kein Antrag), 'pending', 'approved', 'rejected'
```
---
### Tabelle: `breeder_profiles`
```sql
CREATE TABLE breeder_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
kennel_name VARCHAR(100) NOT NULL, -- Zwingername
breed_id INTEGER NOT NULL, -- Nur eine Rasse (FK auf breeds)
vdh_member BOOLEAN DEFAULT FALSE,
association VARCHAR(100), -- Zuchtverein (z.B. KFZ, VDH)
description TEXT, -- Freitext Vorstellung
website VARCHAR(255),
location_lat FLOAT,
location_lng FLOAT,
location_city VARCHAR(100),
show_on_map BOOLEAN DEFAULT TRUE, -- Auf Karte sichtbar
verified_at DATETIME DEFAULT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (breed_id) REFERENCES breeds(id)
);
```
---
### Tabelle: `breeder_documents`
```sql
CREATE TABLE breeder_documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
document_type VARCHAR(50) NOT NULL,
-- Werte: 'vdh_membership', 'breeding_permit'
file_path VARCHAR(255) NOT NULL,
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
```
---
### Tabelle: `litters` (Würfe)
```sql
CREATE TABLE litters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
breeder_id INTEGER NOT NULL, -- FK auf breeder_profiles
breed_id INTEGER NOT NULL,
father_name VARCHAR(100),
mother_name VARCHAR(100),
birth_date DATE,
expected_date DATE, -- Bei geplantem Wurf
total_puppies INTEGER,
available_count INTEGER,
description TEXT,
health_tests TEXT, -- HD, ED, Augen etc. als JSON
price_range VARCHAR(50), -- Optional, z.B. "1200-1500 €"
status VARCHAR(20) DEFAULT 'planned',
-- Werte: 'planned', 'born', 'available', 'closed'
visible BOOLEAN DEFAULT TRUE, -- Züchter steuert Sichtbarkeit
visible_until DATE DEFAULT NULL, -- Optional: Ablaufdatum
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (breeder_id) REFERENCES breeder_profiles(id),
FOREIGN KEY (breed_id) REFERENCES breeds(id)
);
```
---
### Tabelle: `puppies` (Einzelne Welpen — optional pro Wurf)
```sql
CREATE TABLE puppies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
litter_id INTEGER NOT NULL,
name VARCHAR(100), -- Optional
gender VARCHAR(10), -- 'male', 'female'
color VARCHAR(50),
chip_number VARCHAR(50),
birth_weight FLOAT, -- Gramm
status VARCHAR(20) DEFAULT 'available',
-- Werte: 'available', 'reserved', 'adopted'
show_status BOOLEAN DEFAULT TRUE, -- Züchter entscheidet ob Status sichtbar
notes TEXT, -- Interne Notiz des Züchters
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (litter_id) REFERENCES litters(id)
);
```
---
### Tabelle: `puppy_weights` (Gewichtsverlauf Welpen)
```sql
CREATE TABLE puppy_weights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
puppy_id INTEGER NOT NULL,
weight_g FLOAT NOT NULL,
measured_at DATE NOT NULL,
FOREIGN KEY (puppy_id) REFERENCES puppies(id)
);
```
---
### Tabelle: `breeder_photos` (Fotos — Züchter, Würfe, Welpen, Elterntiere)
```sql
CREATE TABLE breeder_photos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
breeder_id INTEGER NOT NULL, -- FK auf breeder_profiles (Eigentümer)
entity_type VARCHAR(20) NOT NULL,
-- Werte:
-- 'breeder' → Züchter-Profilbild / Zwinger-Fotos
-- 'litter' → Wurf-Galerie (Geschwister, Wurfbox)
-- 'puppy' → Einzelner Welpe
-- 'parent' → Elterntier (Mutter oder Vater)
entity_id INTEGER NOT NULL, -- ID des jeweiligen Datensatzes
file_path VARCHAR(255) NOT NULL, -- Gespeicherter Pfad
thumbnail_path VARCHAR(255), -- Automatisch generiertes Thumbnail
caption VARCHAR(255), -- Optionale Bildunterschrift
is_primary BOOLEAN DEFAULT FALSE, -- Hauptbild des Datensatzes
visibility VARCHAR(20) DEFAULT 'public',
-- Werte:
-- 'public' → Für alle sichtbar
-- 'inquiry' → Nur nach Kontaktaufnahme sichtbar
-- 'private' → Nur für den Züchter selbst (interne Fotos)
sort_order INTEGER DEFAULT 0, -- Reihenfolge in der Galerie
uploaded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (breeder_id) REFERENCES breeder_profiles(id)
);
-- Index für schnelle Abfragen
CREATE INDEX idx_breeder_photos_entity ON breeder_photos(entity_type, entity_id);
```
**Erklärung visibility-Stufen:**
- `public` — sofort für alle Besucher sichtbar (z.B. Wurf-Übersichtsbild)
- `inquiry` — erst nach erster Nachricht des Interessenten freigeschaltet (z.B. detaillierte Welpen-Fotos)
- `private` — nur für den Züchter selbst, z.B. interne Entwicklungsfotos, Wiegefotos
---
### Tabelle: `breeder_inquiries` (Interessenten)
```sql
CREATE TABLE breeder_inquiries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
litter_id INTEGER NOT NULL,
sender_id INTEGER NOT NULL, -- User der anfrägt
message TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'new',
-- Werte: 'new', 'replied', 'reserved', 'closed'
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (litter_id) REFERENCES litters(id),
FOREIGN KEY (sender_id) REFERENCES users(id)
);
```
---
## 3. Registrierungsprozess
### Schritt 1 — Antrag stellen (Frontend)
Der User navigiert zu "Mein Profil → Züchter werden".
**Formular:**
```
Zwingername *
Zuchtverein / Organisation *
Rasse * (Dropdown aus breeds-Tabelle)
VDH-Mitglied? (Ja / Nein)
Stadt / Region *
Website (optional)
Kurze Vorstellung (Freitext)
Dokumente hochladen:
[ ] VDH-Mitgliedsausweis (PDF oder Bild)
[ ] Zuchtzulassung des Vereins (PDF oder Bild)
→ Mindestens eines der beiden Dokumente erforderlich
[ ] Ich bestätige, dass meine Angaben korrekt sind
[ ] Ich habe die Nutzungsbedingungen für Züchter gelesen
[Antrag absenden]
```
---
### Schritt 2 — Backend-Verarbeitung
```python
# POST /api/breeder/apply
@router.post("/breeder/apply")
async def apply_for_breeder(
current_user: User = Depends(get_current_user),
form_data: BreederApplicationForm,
documents: List[UploadFile]
):
# Validierung
if current_user.breeder_status == 'pending':
raise HTTPException(400, "Antrag bereits gestellt")
if current_user.role == 'breeder':
raise HTTPException(400, "Bereits verifizierter Züchter")
if len(documents) == 0:
raise HTTPException(400, "Mindestens ein Dokument erforderlich")
# Dokumente speichern (nicht öffentlich erreichbar)
for doc in documents:
save_path = f"/private/breeder_docs/{current_user.id}/{doc.filename}"
save_document(doc, save_path)
db.add(BreederDocument(
user_id=current_user.id,
document_type=detect_doc_type(doc.filename),
file_path=save_path
))
# Profil-Entwurf anlegen
db.add(BreederProfile(
user_id=current_user.id,
kennel_name=form_data.kennel_name,
breed_id=form_data.breed_id,
...
))
# Status setzen
current_user.breeder_status = 'pending'
db.commit()
# Admin benachrichtigen
await notify_admin_new_breeder_application(current_user, form_data)
return {"message": "Antrag eingereicht. Du wirst per E-Mail benachrichtigt."}
```
---
### Schritt 3 — Admin-Benachrichtigung
**E-Mail an René:**
```
Betreff: [Banyaro] Neuer Züchter-Antrag — {Zwingername}
Neuer Antrag von: {Username} ({E-Mail})
Zwingername: {Zwingername}
Rasse: {Rasse}
Verein: {Verein}
VDH: Ja/Nein
Dokumente: {Anzahl} hochgeladen
→ Im Admin-Bereich prüfen: banyaro.app/admin/breeders
```
---
### Schritt 4 — Admin-Bereich (Prüfung)
**Route:** `/admin/breeders`
**Ansicht je Antrag:**
- Antragsdaten vollständig anzeigen
- Dokumente inline anzeigen (PDF-Viewer oder Bild)
- Buttons: **[Freischalten]** | **[Ablehnen]**
- Freitextfeld für Ablehnungsgrund (wird per E-Mail gesendet)
```python
# POST /api/admin/breeder/{user_id}/approve
@router.post("/admin/breeder/{user_id}/approve")
async def approve_breeder(
user_id: int,
admin: User = Depends(require_admin)
):
user = db.get(User, user_id)
user.role = 'breeder'
user.breeder_status = 'approved'
profile = db.query(BreederProfile).filter_by(user_id=user_id).first()
profile.verified_at = datetime.utcnow()
db.commit()
await send_breeder_approval_email(user)
return {"message": "Züchter freigeschaltet"}
# POST /api/admin/breeder/{user_id}/reject
@router.post("/admin/breeder/{user_id}/reject")
async def reject_breeder(
user_id: int,
reason: str,
admin: User = Depends(require_admin)
):
user = db.get(User, user_id)
user.breeder_status = 'rejected'
db.commit()
await send_breeder_rejection_email(user, reason)
return {"message": "Antrag abgelehnt"}
```
---
### Schritt 5 — User-Benachrichtigung
**Bei Freischaltung:**
```
Betreff: Willkommen als Züchter bei Banyaro! 🐾
Hallo {Username},
dein Züchter-Profil wurde erfolgreich verifiziert.
Ab sofort hast du Zugang zu allen Züchter-Features:
✅ Züchter-Profil auf der Karte
✅ Wurf-Verwaltung
✅ Wurfankündigungen
✅ Interessenten-Nachrichten
→ Jetzt Profil vervollständigen: banyaro.app/breeder/profile
Viel Erfolg mit deiner Zucht!
Das Banyaro-Team
```
**Bei Ablehnung:**
```
Betreff: Dein Züchter-Antrag bei Banyaro
Hallo {Username},
leider konnten wir deinen Antrag aktuell nicht bestätigen.
Grund: {Ablehnungsgrund}
Du kannst einen neuen Antrag stellen sobald du die
fehlenden Dokumente vorliegen hast.
Bei Fragen: mail@motocamp.de
```
---
## 4. Züchter-Features im Detail
### 4.1 Öffentliches Züchter-Profil
**Route:** `/breeder/{kennel_name}`
**Anzeige:**
- Zwingername + verifiziertes Badge ✓
- Rasse (verlinkt auf Wiki-Eintrag)
- Zuchtverein
- Stadt / Region
- Beschreibung
- Website-Link
- Aktive Wurfankündigungen
- Kontakt-Button → öffnet internes Chat
**Auf der Karte:**
- Eigener Marker-Typ "Züchter" (unterschiedliches Icon)
- Popup mit Zwingername, Rasse, Link zum Profil
---
### 4.2 Wurf-Verwaltung
**Route:** `/breeder/litters`
**Funktionen:**
- Neuen Wurf anlegen (geplant oder bereits geboren)
- Einzelne Welpen anlegen (optional)
- Gewichtsverlauf pro Welpe erfassen
- Status pro Welpe: verfügbar / reserviert / abgegeben
- Züchter steuert selbst was öffentlich sichtbar ist
- Wurf archivieren (bleibt als Referenz erhalten)
---
### 4.3 Wurfankündigung (öffentlich)
**Route:** `/litters` (öffentliche Übersicht für alle User)
**Filter:**
- Nach Rasse
- Nach PLZ / Umkreis
- Nach Status (geplant / verfügbar)
**Detailseite pro Wurf:**
- Elterntiere (Name, Foto optional)
- Geburtsdatum / erwarteter Termin
- Anzahl Welpen gesamt / verfügbar
- Gesundheitsuntersuchungen der Eltern
- Preisrahmen (optional)
- Fotos
- [Nachricht senden]-Button
---
### 4.4 Interessenten-Verwaltung
Eingehende Anfragen erscheinen im internen Chat-System.
Züchter kann Status der Anfrage setzen: neu / beantwortet / reserviert / abgeschlossen.
---
### 4.5 Foto-Verwaltung
**Wo Fotos hochgeladen werden können:**
| Kontext | entity_type | Beispiel |
|---|---|---|
| Züchter-Profilbild / Zwinger | `breeder` | Foto des Zwingers, Teamfoto |
| Wurf allgemein | `litter` | Wurfbox, alle Welpen zusammen |
| Einzelner Welpe | `puppy` | Porträtfoto, Entwicklungsfotos |
| Elterntier | `parent` | Mutter, Vater |
**Foto hochladen (Backend):**
```python
# POST /api/breeder/photos/upload
@router.post("/breeder/photos/upload")
async def upload_photo(
entity_type: str, # 'breeder', 'litter', 'puppy', 'parent'
entity_id: int,
visibility: str, # 'public', 'inquiry', 'private'
caption: str = None,
is_primary: bool = False,
file: UploadFile = File(...),
breeder: User = Depends(require_breeder)
):
# Validierung: entity gehört dem Züchter
validate_entity_ownership(entity_type, entity_id, breeder.id)
# Bildverarbeitung
img = Image.open(file.file)
img = resize_and_optimize(img, max_size=(1200, 1200))
thumbnail = generate_thumbnail(img, size=(300, 300))
# Speichern
file_path = f"/uploads/breeders/{breeder.id}/{entity_type}/{entity_id}/{uuid4()}.webp"
thumb_path = file_path.replace(".webp", "_thumb.webp")
save_image(img, file_path)
save_image(thumbnail, thumb_path)
# Wenn is_primary: bisheriges Hauptbild zurücksetzen
if is_primary:
db.query(BreederPhoto).filter_by(
entity_type=entity_type,
entity_id=entity_id,
is_primary=True
).update({"is_primary": False})
db.add(BreederPhoto(
breeder_id=breeder.id,
entity_type=entity_type,
entity_id=entity_id,
file_path=file_path,
thumbnail_path=thumb_path,
caption=caption,
is_primary=is_primary,
visibility=visibility
))
db.commit()
return {"message": "Foto hochgeladen", "file_path": file_path}
```
**Foto abrufen (mit Sichtbarkeitslogik):**
```python
# GET /api/photos/{entity_type}/{entity_id}
@router.get("/photos/{entity_type}/{entity_id}")
async def get_photos(
entity_type: str,
entity_id: int,
current_user: User = Depends(get_optional_user) # auch ohne Login
):
query = db.query(BreederPhoto).filter_by(
entity_type=entity_type,
entity_id=entity_id
)
# Sichtbarkeitsfilter
if current_user and current_user.role == 'breeder':
# Züchter sieht alle eigenen Fotos
pass
elif current_user and has_sent_inquiry(current_user.id, entity_id):
# Interessent mit Anfrage sieht public + inquiry
query = query.filter(BreederPhoto.visibility.in_(['public', 'inquiry']))
else:
# Alle anderen sehen nur public
query = query.filter_by(visibility='public')
return query.order_by(
BreederPhoto.is_primary.desc(),
BreederPhoto.sort_order
).all()
```
**Sichtbarkeit ändern:**
```python
# PATCH /api/breeder/photos/{photo_id}/visibility
@router.patch("/breeder/photos/{photo_id}/visibility")
async def update_photo_visibility(
photo_id: int,
visibility: str, # 'public', 'inquiry', 'private'
breeder: User = Depends(require_breeder)
):
photo = db.get(BreederPhoto, photo_id)
if photo.breeder_id != breeder.breeder_profile.id:
raise HTTPException(403, "Nicht berechtigt")
photo.visibility = visibility
db.commit()
return {"message": "Sichtbarkeit aktualisiert"}
```
**Frontend — Galerie-Ansicht für Interessenten:**
Auf der öffentlichen Wurfseite:
- Öffentliche Fotos sofort sichtbar als Galerie
- Inquiry-Fotos erscheinen als gesperrte Vorschau: *"3 weitere Fotos nach Kontaktaufnahme"*
- Nach erster Nachricht: automatisch freigeschaltet ohne weiteres Zutun des Züchters
**Frontend — Foto-Verwaltung für Züchter:**
- Drag & Drop Reihenfolge ändern
- Pro Foto: Sichtbarkeit umschalten (öffentlich / nach Anfrage / privat)
- Hauptbild markieren (wird in Listen- und Kartenansicht gezeigt)
- Bildunterschrift bearbeiten
- Foto löschen
---
### 4.6 Läufigkeits-Tracker (Züchter-Erweiterung)
Der bestehende Läufigkeits-Tracker wird für Züchter erweitert:
- Deckdatum erfassen
- Automatische Berechnung des erwarteten Wurftermins (63 Tage Trächtigkeitsdauer)
- Erinnerung: "Wurftermin in 7 Tagen"
- Direkte Verknüpfung: Läufigkeit → neuen Wurf anlegen
---
## 5. API-Endpunkte Übersicht
```
# Registrierung
POST /api/breeder/apply Antrag stellen
GET /api/breeder/status Antragsstatus abfragen
# Admin
GET /api/admin/breeders/pending Offene Anträge
POST /api/admin/breeder/{id}/approve Freischalten
POST /api/admin/breeder/{id}/reject Ablehnen
# Profil
GET /api/breeder/{kennel_name} Öffentliches Profil
PUT /api/breeder/profile Profil bearbeiten
# Würfe
GET /api/litters Öffentliche Übersicht
GET /api/litters/{id} Wurf-Detail
POST /api/breeder/litters Neuen Wurf anlegen
PUT /api/breeder/litters/{id} Wurf bearbeiten
DELETE /api/breeder/litters/{id} Wurf archivieren
# Welpen
POST /api/breeder/litters/{id}/puppies Welpe anlegen
PUT /api/breeder/puppies/{id} Welpe bearbeiten
POST /api/breeder/puppies/{id}/weight Gewicht erfassen
# Fotos
POST /api/breeder/photos/upload Foto hochladen
GET /api/photos/{entity_type}/{entity_id} Fotos abrufen (mit Sichtbarkeitslogik)
PATCH /api/breeder/photos/{id}/visibility Sichtbarkeit ändern
PATCH /api/breeder/photos/{id}/primary Als Hauptbild setzen
PATCH /api/breeder/photos/{id}/caption Bildunterschrift ändern
PATCH /api/breeder/photos/reorder Reihenfolge ändern (Array von IDs)
DELETE /api/breeder/photos/{id} Foto löschen
# Karte
GET /api/map/breeders Züchter für Karte
```
---
## 6. Berechtigungsprüfung (Middleware)
```python
def require_breeder(current_user: User = Depends(get_current_user)):
if current_user.role not in ('breeder', 'admin'):
raise HTTPException(
status_code=403,
detail="Diese Funktion ist nur für verifizierte Züchter verfügbar"
)
return current_user
```
Verwendung:
```python
@router.post("/breeder/litters")
async def create_litter(
breeder: User = Depends(require_breeder),
...
):
```
---
## 7. Sicherheit & Datenschutz
- Hochgeladene Dokumente werden **außerhalb des öffentlichen Webroot** gespeichert
- Nur Admin kann Dokumente abrufen (authentifizierter Endpunkt)
- Dokumente werden nach Prüfung nicht dauerhaft benötigt — optional nach 90 Tagen löschen
- Standort des Züchters: nur Stadt/Region öffentlich, keine genaue Adresse
- Datenschutzerklärung um Züchter-Daten ergänzen
---
## 8. Umsetzungsreihenfolge (empfohlen)
| Schritt | Was | Aufwand |
|---|---|---|
| 1 | DB-Migration (users, breeder_profiles, breeder_documents) | Klein |
| 2 | Antrag-Formular Frontend + Dokument-Upload | Mittel |
| 3 | Admin-Bereich: Anträge prüfen + freischalten | Mittel |
| 4 | E-Mail-Benachrichtigungen (Antrag, Freischaltung, Ablehnung) | Klein |
| 5 | Öffentliches Züchter-Profil + Karten-Marker | Mittel |
| 6 | Wurf-Verwaltung (CRUD) | Mittel |
| 7 | Öffentliche Wurfankündigung + Filtersuche | Mittel |
| 8 | Welpen-Verwaltung + Gewichtsverlauf | Klein |
| 9 | Foto-System (Upload, Sichtbarkeit, Galerie) | Mittel |
| 10 | Läufigkeits-Tracker Erweiterung | Klein |
| 11 | Interessenten-Chat-Integration + Foto-Freischaltung | Klein |
**Gesamtaufwand geschätzt:** 46 Tage mit Claude Code (Basis)
---
## 9. Erweiterte Züchter-Features — Stammbaum & Genetik
---
### 9.1 Datenbankstruktur Erweiterung
#### Tabelle: `dogs` (Hunde-Stammdaten — zentral für Stammbaum)
```sql
CREATE TABLE dogs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
breeder_id INTEGER, -- NULL wenn fremder Hund
name VARCHAR(100) NOT NULL,
call_name VARCHAR(50), -- Rufname
breed_id INTEGER NOT NULL,
gender VARCHAR(10), -- 'male', 'female'
date_of_birth DATE,
date_of_death DATE,
chip_number VARCHAR(50),
tattoo_number VARCHAR(50),
stud_book_number VARCHAR(50), -- Zuchtbuchnummer
color VARCHAR(50),
father_id INTEGER, -- FK auf dogs.id
mother_id INTEGER, -- FK auf dogs.id
breeder_name VARCHAR(100), -- Fremdzüchter (Text)
owner_name VARCHAR(100), -- Aktueller Eigentümer
is_public BOOLEAN DEFAULT TRUE, -- Im Stammbaum sichtbar
notes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (breeder_id) REFERENCES breeder_profiles(id),
FOREIGN KEY (breed_id) REFERENCES breeds(id),
FOREIGN KEY (father_id) REFERENCES dogs(id),
FOREIGN KEY (mother_id) REFERENCES dogs(id)
);
```
---
#### Tabelle: `dog_health_tests` (Gesundheitsuntersuchungen)
```sql
CREATE TABLE dog_health_tests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
test_type VARCHAR(50) NOT NULL,
-- Standardwerte: 'HD', 'ED', 'OCD', 'eyes', 'heart', 'patella',
-- 'spine', 'DNA_MDR1', 'DNA_PRA', 'DNA_DM', 'ZTP', 'custom'
test_name VARCHAR(100), -- Bei 'custom': freier Name
result VARCHAR(50) NOT NULL,
-- HD: 'A1','A2','B1','B2','C1','C2','D1','D2','E1','E2'
-- ED: '0','1','2','3'
-- DNA: 'clear','carrier','affected'
-- ZTP: 'bestanden','nicht bestanden'
-- Augen: 'frei','verdächtig','betroffen'
examined_at DATE NOT NULL,
valid_until DATE, -- Bei zeitlich begrenzten Tests
examined_by VARCHAR(100), -- Tierarzt / Institut
lab_name VARCHAR(100), -- Labor (z.B. Laboklin, Genoline)
certificate_no VARCHAR(50), -- Zertifikatsnummer
document_path VARCHAR(255), -- PDF-Upload
is_public BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (dog_id) REFERENCES dogs(id)
);
```
---
#### Tabelle: `dog_titles` (Auszeichnungen & Titel)
```sql
CREATE TABLE dog_titles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
title_type VARCHAR(50) NOT NULL,
-- Werte: 'show', 'working', 'sport', 'breeding', 'champion', 'custom'
title_name VARCHAR(100) NOT NULL,
-- Beispiele: 'CAC', 'CACIB', 'BOB', 'BIS', 'Bundesieger',
-- 'IPO1', 'IPO2', 'IPO3', 'BH', 'AD', 'Championat'
awarded_at DATE NOT NULL,
location VARCHAR(100), -- Ausstellungsort
judge_name VARCHAR(100), -- Richter
show_name VARCHAR(100), -- Name der Veranstaltung
grade VARCHAR(20), -- Formwert: V, SG, G, A
document_path VARCHAR(255), -- Urkunde hochladen
is_public BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (dog_id) REFERENCES dogs(id)
);
```
---
#### Tabelle: `dog_genetic_tests` (Genetische Tests / DNA)
```sql
CREATE TABLE dog_genetic_tests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dog_id INTEGER NOT NULL,
marker_name VARCHAR(100) NOT NULL,
-- Beispiele: 'MDR1', 'PRA-prcd', 'DM', 'vWD', 'HUU',
-- 'Fell_Locus_A', 'Fell_Locus_B', 'Fell_Locus_E'
marker_category VARCHAR(50),
-- Werte: 'disease', 'carrier_test', 'color', 'trait'
genotype VARCHAR(50), -- Rohwert z.B. '+/+', '+/-', '-/-'
result_class VARCHAR(20),
-- Werte: 'clear', 'carrier', 'affected', 'homozygous', 'heterozygous'
tested_at DATE NOT NULL,
lab_name VARCHAR(100),
certificate_no VARCHAR(50),
document_path VARCHAR(255),
is_public BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (dog_id) REFERENCES dogs(id)
);
```
---
### 9.2 Stammbaum-Darstellung
#### Algorithmus: Stammbaum aufbauen (rekursiv)
```python
# GET /api/dogs/{dog_id}/pedigree?generations=4
@router.get("/dogs/{dog_id}/pedigree")
async def get_pedigree(
dog_id: int,
generations: int = 4, # Standard: 4 Generationen
current_user: User = Depends(get_optional_user)
):
def build_tree(dog_id: int, depth: int) -> dict:
if depth == 0 or dog_id is None:
return None
dog = db.get(Dog, dog_id)
if not dog:
return None
node = {
"id": dog.id,
"name": dog.name,
"call_name": dog.call_name,
"gender": dog.gender,
"date_of_birth": dog.date_of_birth,
"stud_book_number": dog.stud_book_number,
"color": dog.color,
"photo": get_primary_photo(dog.id),
"health_summary": get_health_summary(dog.id), # HD, ED als Kurzform
"titles": get_title_summary(dog.id), # Wichtigste Titel
"inbreeding_coefficient": None, # Wird separat berechnet
"father": build_tree(dog.father_id, depth - 1),
"mother": build_tree(dog.mother_id, depth - 1),
}
return node
tree = build_tree(dog_id, generations)
return tree
```
#### Frontend-Darstellung
Der Stammbaum wird als **horizontaler Baum** von links nach rechts dargestellt:
```
Generation 1 Generation 2 Generation 3 Generation 4
[Hund] ───── [Vater] ───── [GV väterl.]── [UGV]
[UGM]
[GM väterl.]── [UGV]
[UGM]
───── [Mutter] ───── [GV mütterl.]─ [UGV]
[UGM]
[GM mütterl.]─ [UGV]
[UGM]
```
Jeder Knoten zeigt:
- Name + Zuchtbuchnummer
- Foto (Thumbnail)
- HD/ED-Ergebnis als farbiger Badge (grün/gelb/rot)
- Wichtigster Titel (z.B. "V, CAC")
- Klick → öffnet vollständiges Hunde-Profil
---
### 9.3 Inzuchtkoeffizient (IK) Berechnung
Der Inzuchtkoeffizient nach Wright gibt an wie hoch der Anteil identischer Erbanlagen durch gemeinsame Vorfahren ist. 0% = keine Inzucht, >6.25% = erhöht, >12.5% = kritisch.
```python
# Berechnung nach Wright's Formel
# F(I) = Σ [(0.5)^(n1+n2+1) * (1 + F(A))]
# n1 = Generationsabstand Vater zum gemeinsamen Vorfahren
# n2 = Generationsabstand Mutter zum gemeinsamen Vorfahren
# F(A) = IK des gemeinsamen Vorfahren selbst
def calculate_inbreeding_coefficient(dog_id: int, generations: int = 10) -> float:
"""
Berechnet den Inzuchtkoeffizienten nach Wright.
generations: Wie viele Generationen zurück gesucht wird (max. 10 empfohlen)
"""
def get_ancestors(dog_id, depth, path=[]) -> dict:
"""Gibt alle Vorfahren mit ihren Pfaden zurück"""
if depth == 0 or dog_id is None:
return {}
dog = db.get(Dog, dog_id)
if not dog:
return {}
ancestors = {dog_id: [path]}
for parent_id in [dog.father_id, dog.mother_id]:
if parent_id:
sub = get_ancestors(parent_id, depth - 1, path + [dog_id])
for ancestor_id, paths in sub.items():
if ancestor_id in ancestors:
ancestors[ancestor_id].extend(paths)
else:
ancestors[ancestor_id] = paths
return ancestors
dog = db.get(Dog, dog_id)
if not dog or not dog.father_id or not dog.mother_id:
return 0.0
# Vorfahren beider Elternteile getrennt ermitteln
father_ancestors = get_ancestors(dog.father_id, generations, [])
mother_ancestors = get_ancestors(dog.mother_id, generations, [])
# Gemeinsame Vorfahren finden
common_ancestors = set(father_ancestors.keys()) & set(mother_ancestors.keys())
coefficient = 0.0
for ancestor_id in common_ancestors:
fa_ancestor_ik = calculate_inbreeding_coefficient(ancestor_id, generations - 1)
for path_f in father_ancestors[ancestor_id]:
for path_m in mother_ancestors[ancestor_id]:
n1 = len(path_f)
n2 = len(path_m)
coefficient += (0.5 ** (n1 + n2 + 1)) * (1 + fa_ancestor_ik)
return round(coefficient * 100, 2) # Als Prozentwert
# API-Endpunkt
# GET /api/dogs/{dog_id}/inbreeding?generations=10
@router.get("/dogs/{dog_id}/inbreeding")
async def get_inbreeding(dog_id: int, generations: int = 10):
ik = calculate_inbreeding_coefficient(dog_id, generations)
return {
"dog_id": dog_id,
"inbreeding_coefficient": ik,
"generations_analyzed": generations,
"rating": get_ik_rating(ik) # 'optimal', 'acceptable', 'elevated', 'critical'
}
def get_ik_rating(ik: float) -> str:
if ik < 2.5: return "optimal"
if ik < 6.25: return "acceptable"
if ik < 12.5: return "elevated"
return "critical"
```
---
### 9.4 Probeverpaarung
```python
# POST /api/breeder/trial-mating
# Berechnet IK und Gesundheitsrisiken für eine geplante Verpaarung
@router.post("/breeder/trial-mating")
async def trial_mating(
father_id: int,
mother_id: int,
breeder: User = Depends(require_breeder)
):
father = db.get(Dog, father_id)
mother = db.get(Dog, mother_id)
# Temporären "Dummy"-Hund für IK-Berechnung erstellen
dummy = Dog(father_id=father_id, mother_id=mother_id)
ik = calculate_inbreeding_coefficient_for_parents(father_id, mother_id)
# Genetische Risiken analysieren
genetic_risks = analyze_genetic_compatibility(father_id, mother_id)
# Gemeinsame Vorfahren für Transparenz
common_ancestors = find_common_ancestors(father_id, mother_id, generations=6)
return {
"father": {"id": father.id, "name": father.name},
"mother": {"id": mother.id, "name": mother.name},
"projected_inbreeding_coefficient": ik,
"ik_rating": get_ik_rating(ik),
"genetic_risks": genetic_risks,
"common_ancestors": common_ancestors,
"recommendation": get_mating_recommendation(ik, genetic_risks)
}
def analyze_genetic_compatibility(father_id: int, mother_id: int) -> list:
"""
Prüft ob beide Eltern Träger der gleichen rezessiven Erbkrankheit sind.
Träger x Träger = 25% Risiko für betroffene Nachkommen
"""
father_tests = db.query(DogGeneticTest).filter_by(dog_id=father_id).all()
mother_tests = db.query(DogGeneticTest).filter_by(dog_id=mother_id).all()
risks = []
for ft in father_tests:
for mt in mother_tests:
if ft.marker_name == mt.marker_name:
risk = calculate_risk(ft.result_class, mt.result_class)
if risk:
risks.append({
"marker": ft.marker_name,
"father_status": ft.result_class,
"mother_status": mt.result_class,
"offspring_risk": risk,
"risk_level": get_risk_level(risk)
})
return risks
def calculate_risk(status_a: str, status_b: str) -> str:
"""Mendel'sche Genetik: Träger x Träger etc."""
combos = {
("clear", "clear"): None,
("clear", "carrier"): "0% betroffen, 50% Träger",
("carrier", "clear"): "0% betroffen, 50% Träger",
("carrier", "carrier"): "25% betroffen, 50% Träger",
("clear", "affected"): "0% betroffen, 100% Träger",
("affected", "clear"): "0% betroffen, 100% Träger",
("carrier", "affected"): "50% betroffen, 50% Träger",
("affected", "carrier"): "50% betroffen, 50% Träger",
("affected", "affected"): "100% betroffen",
}
return combos.get((status_a, status_b))
```
---
### 9.5 Öffentliches Hunde-Profil
**Route:** `/dogs/{dog_id}` oder `/dogs/{stud_book_number}`
**Anzeige:**
- Name, Rufname, Rasse, Geschlecht, Geburtsdatum
- Fotos (Galerie)
- Züchter (verlinkt auf Zwinger-Profil)
- Stammbaum (4 Generationen, aufklappbar)
- Gesundheitstests (Tabelle: Test, Ergebnis, Datum, Attest)
- Genetische Tests (Marker, Status als farbige Badges)
- Titel & Auszeichnungen (chronologisch)
- Würfe (wenn Zuchthündin oder -rüde)
**Badges für Gesundheitswerte:**
```
HD: [A] grün [B] hellgrün [C] gelb [D] orange [E] rot
ED: [0] grün [1] gelb [2] orange [3] rot
DNA: [Frei] grün [Träger] gelb [Betroffen] rot
```
---
### 9.6 Automatischer Kaufvertrag
```python
# GET /api/breeder/puppies/{puppy_id}/contract?buyer_name=...&buyer_address=...
@router.get("/breeder/puppies/{puppy_id}/contract")
async def generate_contract(
puppy_id: int,
buyer_name: str,
buyer_address: str,
buyer_email: str,
price: float,
breeder: User = Depends(require_breeder)
):
puppy = db.get(Puppy, puppy_id)
litter = db.get(Litter, puppy.litter_id)
breeder_profile = db.get(BreederProfile, breeder.breeder_profile.id)
contract_data = {
"breeder_name": breeder.full_name,
"breeder_address": breeder_profile.location_city,
"kennel_name": breeder_profile.kennel_name,
"buyer_name": buyer_name,
"buyer_address": buyer_address,
"buyer_email": buyer_email,
"puppy_name": puppy.name,
"puppy_gender": puppy.gender,
"puppy_dob": litter.birth_date,
"breed": breeder_profile.breed.name,
"chip_number": puppy.chip_number,
"price": price,
"contract_date": date.today()
}
# PDF aus Vorlage generieren (Pillow / reportlab)
pdf_path = generate_contract_pdf(contract_data)
return FileResponse(pdf_path, media_type="application/pdf")
```
---
### 9.7 Neue API-Endpunkte (Ergänzung)
```
# Hunde-Stammdaten
POST /api/breeder/dogs Hund anlegen
PUT /api/breeder/dogs/{id} Hund bearbeiten
GET /api/dogs/{id} Öffentliches Profil
GET /api/dogs/{id}/pedigree Stammbaum (n Generationen)
GET /api/dogs/{id}/inbreeding Inzuchtkoeffizient
# Gesundheitstests
POST /api/breeder/dogs/{id}/health-tests Test erfassen
PUT /api/breeder/health-tests/{id} Test bearbeiten
DELETE /api/breeder/health-tests/{id} Test löschen
# Genetische Tests
POST /api/breeder/dogs/{id}/genetic-tests Gentest erfassen
PUT /api/breeder/genetic-tests/{id} Test bearbeiten
# Titel & Auszeichnungen
POST /api/breeder/dogs/{id}/titles Titel erfassen
PUT /api/breeder/titles/{id} Titel bearbeiten
DELETE /api/breeder/titles/{id} Titel löschen
# Probeverpaarung
POST /api/breeder/trial-mating IK + Risiken berechnen
# Kaufvertrag
GET /api/breeder/puppies/{id}/contract PDF generieren
```
---
### 9.8 Aktualisierte Umsetzungsreihenfolge (komplett)
| Schritt | Was | Aufwand |
|---|---|---|
| 1 | DB-Migration Basis (users, breeder_profiles, breeder_documents) | Klein |
| 2 | Antrag-Formular Frontend + Dokument-Upload | Mittel |
| 3 | Admin-Bereich: Anträge prüfen + freischalten | Mittel |
| 4 | E-Mail-Benachrichtigungen | Klein |
| 5 | Öffentliches Züchter-Profil + Karten-Marker | Mittel |
| 6 | Hunde-Stammdaten (dogs-Tabelle, CRUD) | Mittel |
| 7 | Stammbaum-Darstellung (rekursiv, Frontend) | Groß |
| 8 | Inzuchtkoeffizient-Berechnung | Mittel |
| 9 | Gesundheitstests + Genetische Tests erfassen | Mittel |
| 10 | Titel & Auszeichnungen | Klein |
| 11 | Probeverpaarung (IK + Risikoanalyse) | Mittel |
| 12 | Wurf-Verwaltung (CRUD) | Mittel |
| 13 | Öffentliche Wurfankündigung + Filtersuche | Mittel |
| 14 | Welpen-Verwaltung + Gewichtsverlauf | Klein |
| 15 | Foto-System (Upload, Sichtbarkeit, Galerie) | Mittel |
| 16 | Läufigkeits-Tracker Erweiterung | Klein |
| 17 | Interessenten-Chat-Integration | Klein |
| 18 | Automatischer Kaufvertrag (PDF) | Mittel |
**Gesamtaufwand geschätzt:** 812 Tage mit Claude Code