# 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
