1132 lines
36 KiB
Markdown
1132 lines
36 KiB
Markdown
# 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
|