From 58cb2b4ad3b07d1fb033942706f840b31cb9e0fa Mon Sep 17 00:00:00 2001 From: rene Date: Tue, 28 Apr 2026 08:32:31 +0200 Subject: [PATCH] =?UTF-8?q?Dateien=20nach=20=E2=80=9Epromotion=E2=80=9C=20?= =?UTF-8?q?hochladen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit erweiterte Züchterfunktionen, Stammbaum Genetik --- .../banyaro_zuechterolle_ausarbeitung.md | 651 +++++++++++++++++- 1 file changed, 647 insertions(+), 4 deletions(-) diff --git a/promotion/banyaro_zuechterolle_ausarbeitung.md b/promotion/banyaro_zuechterolle_ausarbeitung.md index bb6b76a..a6f280e 100644 --- a/promotion/banyaro_zuechterolle_ausarbeitung.md +++ b/promotion/banyaro_zuechterolle_ausarbeitung.md @@ -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:** 3–5 Tage mit Claude Code +**Gesamtaufwand geschätzt:** 4–6 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:** 8–12 Tage mit Claude Code