# 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:** 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