Dateien nach „promotion“ hochladen

erweiterte Züchterfunktionen, Stammbaum Genetik
This commit is contained in:
rene 2026-04-28 08:32:31 +02:00
parent 4ac1c27b75
commit 58cb2b4ad3

View file

@ -130,6 +130,43 @@ CREATE TABLE puppy_weights (
---
### 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 (
@ -394,7 +431,134 @@ Züchter kann Status der Anfrage setzen: neu / beantwortet / reserviert / abgesc
---
### 4.5 Läufigkeits-Tracker (Züchter-Erweiterung)
### 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
@ -432,6 +596,15 @@ 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
```
@ -483,7 +656,477 @@ async def create_litter(
| 6 | Wurf-Verwaltung (CRUD) | Mittel |
| 7 | Öffentliche Wurfankündigung + Filtersuche | Mittel |
| 8 | Welpen-Verwaltung + Gewichtsverlauf | Klein |
| 9 | Läufigkeits-Tracker Erweiterung | Klein |
| 10 | Interessenten-Chat-Integration | Klein |
| 9 | Foto-System (Upload, Sichtbarkeit, Galerie) | Mittel |
| 10 | Läufigkeits-Tracker Erweiterung | Klein |
| 11 | Interessenten-Chat-Integration + Foto-Freischaltung | Klein |
**Gesamtaufwand geschätzt:** 35 Tage mit Claude Code
**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