"""BAN YARO — Tierärzte Routes (user-level, nie löschen)""" import math from fastapi import APIRouter, Depends, HTTPException, Query from pydantic import BaseModel, Field from typing import Optional from database import db from auth import get_current_user router = APIRouter() class TierarztCreate(BaseModel): name: str = Field(..., min_length=1, max_length=200) strasse: Optional[str] = Field(None, max_length=300) plz: Optional[str] = Field(None, max_length=20) ort: Optional[str] = Field(None, max_length=200) telefon: Optional[str] = Field(None, max_length=30) notfall_telefon: Optional[str] = Field(None, max_length=30) email: Optional[str] = Field(None, max_length=254) website: Optional[str] = Field(None, max_length=500) notizen: Optional[str] = Field(None, max_length=5000) ist_notfallpraxis: bool = False opening_hours: Optional[str] = Field(None, max_length=500) lat: Optional[float] = None lon: Optional[float] = None osm_id: Optional[str] = Field(None, max_length=100) class BewertungCreate(BaseModel): gesamt: int wartezeit: Optional[int] = None freundlichkeit: Optional[int] = None kompetenz: Optional[int] = None text: Optional[str] = Field(None, max_length=5000) class TierarztUpdate(BaseModel): name: Optional[str] = Field(None, max_length=200) strasse: Optional[str] = Field(None, max_length=300) plz: Optional[str] = Field(None, max_length=20) ort: Optional[str] = Field(None, max_length=200) telefon: Optional[str] = Field(None, max_length=30) notfall_telefon: Optional[str] = Field(None, max_length=30) email: Optional[str] = Field(None, max_length=254) website: Optional[str] = Field(None, max_length=500) notizen: Optional[str] = Field(None, max_length=5000) ist_notfallpraxis: Optional[bool] = None aktiv: Optional[bool] = None opening_hours: Optional[str] = Field(None, max_length=500) lat: Optional[float] = None lon: Optional[float] = None osm_id: Optional[str] = Field(None, max_length=100) def _fmt_opening_hours(raw: str | None) -> str | None: """Wandelt OSM-opening_hours-String in lesbares Deutsch um. Beispiel: "Mo-Fr 08:00-18:00; Sa 09:00-13:00" → "Mo–Fr 08:00–18:00 · Sa 09:00–13:00" """ if not raw: return None if raw.strip().lower() == "24/7": return "24/7 geöffnet" result = raw.replace(" - ", "–").replace("-", "–", 1) result = "; ".join( part.strip().replace(";", "").replace(",", " · ") for part in raw.split(";") ) return result @router.get("/my-favorite") async def get_my_favorite(user=Depends(get_current_user)): """Favoriten-Tierarzt des Users (oder null).""" with db() as conn: row = conn.execute( """SELECT t.* FROM tieraerzte t JOIN favorite_vets fv ON fv.vet_id = t.id WHERE fv.user_id = ? LIMIT 1""", (user["id"],) ).fetchone() if not row: return None return dict(row) @router.post("/{vet_id}/favorite") async def toggle_favorite(vet_id: int, user=Depends(get_current_user)): """Tierarzt als Favorit setzen oder entfernen (toggle). Gibt {is_favorite: bool} zurück.""" with db() as conn: vet = conn.execute( "SELECT id FROM tieraerzte WHERE id=?", (vet_id,) ).fetchone() if not vet: raise HTTPException(404, "Tierarzt nicht gefunden.") existing = conn.execute( "SELECT 1 FROM favorite_vets WHERE user_id=? AND vet_id=?", (user["id"], vet_id) ).fetchone() if existing: conn.execute( "DELETE FROM favorite_vets WHERE user_id=? AND vet_id=?", (user["id"], vet_id) ) return {"is_favorite": False} else: conn.execute( "INSERT INTO favorite_vets (user_id, vet_id) VALUES (?, ?)", (user["id"], vet_id) ) return {"is_favorite": True} @router.get("") async def list_tieraerzte(user=Depends(get_current_user)): """Alle Tierärzte des Users — aktive zuerst, dann inaktive. Enthält is_favorite.""" with db() as conn: rows = conn.execute( "SELECT * FROM tieraerzte WHERE user_id=? ORDER BY aktiv DESC, name", (user["id"],) ).fetchall() favs = {r["vet_id"] for r in conn.execute( "SELECT vet_id FROM favorite_vets WHERE user_id=?", (user["id"],) ).fetchall()} result = [] for r in rows: d = dict(r) d["is_favorite"] = r["id"] in favs result.append(d) return result @router.get("/osm-nearby") async def osm_nearby( lat: float = Query(...), lon: float = Query(...), radius_km: float = Query(5.0), user=Depends(get_current_user) ): """Findet Tierarzt-POIs aus dem OSM-Cache in der Nähe (max. 10 Treffer).""" with db() as conn: rows = conn.execute( """SELECT osm_id, name, lat, lon, opening_hours, phone, website FROM osm_pois WHERE type = 'tierarzt' AND lat BETWEEN ? AND ? AND lon BETWEEN ? AND ?""", (lat - radius_km / 111.0, lat + radius_km / 111.0, lon - radius_km / 111.0, lon + radius_km / 111.0) ).fetchall() def _dist(r): dlat = (r["lat"] - lat) * math.pi / 180 dlon = (r["lon"] - lon) * math.pi / 180 a = math.sin(dlat/2)**2 + math.cos(lat*math.pi/180) * math.cos(r["lat"]*math.pi/180) * math.sin(dlon/2)**2 return 6371 * 2 * math.asin(math.sqrt(a)) results = [] for r in rows: d = _dist(r) if d <= radius_km: results.append({ "osm_id": r["osm_id"], "name": r["name"], "lat": r["lat"], "lon": r["lon"], "opening_hours": r["opening_hours"], "opening_hours_fmt": _fmt_opening_hours(r["opening_hours"]), "phone": r["phone"], "website": r["website"], "distanz_km": round(d, 2), }) results.sort(key=lambda x: x["distanz_km"]) return results[:10] @router.post("", status_code=201) async def create_tierarzt(data: TierarztCreate, user=Depends(get_current_user)): with db() as conn: conn.execute( """INSERT INTO tieraerzte (user_id, name, strasse, plz, ort, telefon, notfall_telefon, email, website, notizen, ist_notfallpraxis, opening_hours, lat, lon, osm_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", (user["id"], data.name, data.strasse, data.plz, data.ort, data.telefon, data.notfall_telefon, data.email, data.website, data.notizen, int(data.ist_notfallpraxis), data.opening_hours, data.lat, data.lon, data.osm_id) ) row = conn.execute( "SELECT * FROM tieraerzte WHERE user_id=? ORDER BY id DESC LIMIT 1", (user["id"],) ).fetchone() return dict(row) @router.patch("/{tierarzt_id}") async def update_tierarzt(tierarzt_id: int, data: TierarztUpdate, user=Depends(get_current_user)): with db() as conn: entry = conn.execute( "SELECT id FROM tieraerzte WHERE id=? AND user_id=?", (tierarzt_id, user["id"]) ).fetchone() if not entry: raise HTTPException(404, "Tierarzt nicht gefunden.") updates = {k: v for k, v in data.model_dump().items() if v is not None} if "ist_notfallpraxis" in updates: updates["ist_notfallpraxis"] = int(updates["ist_notfallpraxis"]) if "aktiv" in updates: updates["aktiv"] = int(updates["aktiv"]) if not updates: row = conn.execute("SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() return dict(row) set_clause = ", ".join(f"{k}=?" for k in updates) conn.execute( f"UPDATE tieraerzte SET {set_clause} WHERE id=?", list(updates.values()) + [tierarzt_id] ) row = conn.execute("SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() return dict(row) # ------------------------------------------------------------------ # BEWERTUNGEN # ------------------------------------------------------------------ def _refresh_vet_rating(conn, tierarzt_id: int): """Aktualisiert avg_rating und anz_bewertungen in tieraerzte.""" row = conn.execute( """SELECT COUNT(*) AS n, AVG(CAST(gesamt AS REAL)) AS avg FROM tierarzt_bewertungen WHERE tierarzt_id=?""", (tierarzt_id,) ).fetchone() n = row["n"] or 0 avg = row["avg"] or 0.0 conn.execute( "UPDATE tieraerzte SET avg_rating=?, anz_bewertungen=? WHERE id=?", (round(avg, 1), n, tierarzt_id) ) @router.post("/{tierarzt_id}/bewertung", status_code=201) async def create_bewertung(tierarzt_id: int, data: BewertungCreate, user=Depends(get_current_user)): """Bewertung abgeben (1×pro User+Tierarzt, UPSERT).""" if not (1 <= data.gesamt <= 5): raise HTTPException(400, "Gesamtbewertung muss zwischen 1 und 5 liegen.") for field in ("wartezeit", "freundlichkeit", "kompetenz"): val = getattr(data, field) if val is not None and not (1 <= val <= 5): raise HTTPException(400, f"{field} muss zwischen 1 und 5 liegen.") text = (data.text or "").strip()[:500] or None with db() as conn: vet = conn.execute("SELECT id FROM tieraerzte WHERE id=?", (tierarzt_id,)).fetchone() if not vet: raise HTTPException(404, "Tierarzt nicht gefunden.") conn.execute( """INSERT INTO tierarzt_bewertungen (tierarzt_id, user_id, gesamt, wartezeit, freundlichkeit, kompetenz, text) VALUES (?,?,?,?,?,?,?) ON CONFLICT(tierarzt_id, user_id) DO UPDATE SET gesamt=excluded.gesamt, wartezeit=excluded.wartezeit, freundlichkeit=excluded.freundlichkeit, kompetenz=excluded.kompetenz, text=excluded.text, created_at=datetime('now')""", (tierarzt_id, user["id"], data.gesamt, data.wartezeit, data.freundlichkeit, data.kompetenz, text) ) _refresh_vet_rating(conn, tierarzt_id) row = conn.execute( "SELECT * FROM tieraerzte WHERE id=?", (tierarzt_id,) ).fetchone() return dict(row) @router.get("/{tierarzt_id}/bewertungen") async def list_bewertungen(tierarzt_id: int): """Alle Bewertungen für einen Tierarzt (public). Gibt Zusammenfassung + letzte 5 Texte.""" with db() as conn: vet = conn.execute( "SELECT id, avg_rating, anz_bewertungen FROM tieraerzte WHERE id=?", (tierarzt_id,) ).fetchone() if not vet: raise HTTPException(404, "Tierarzt nicht gefunden.") # Stern-Verteilung verteilung = {} for star in range(1, 6): r = conn.execute( "SELECT COUNT(*) AS n FROM tierarzt_bewertungen WHERE tierarzt_id=? AND gesamt=?", (tierarzt_id, star) ).fetchone() verteilung[str(star)] = r["n"] # Letzte 5 Kommentare kommentare = conn.execute( """SELECT gesamt, wartezeit, freundlichkeit, kompetenz, text, created_at FROM tierarzt_bewertungen WHERE tierarzt_id=? AND text IS NOT NULL AND text != '' ORDER BY created_at DESC LIMIT 5""", (tierarzt_id,) ).fetchall() return { "avg_rating": vet["avg_rating"] or 0, "anz_bewertungen": vet["anz_bewertungen"] or 0, "verteilung": verteilung, "kommentare": [dict(k) for k in kommentare], } @router.get("/{tierarzt_id}/meine-bewertung") async def get_meine_bewertung(tierarzt_id: int, user=Depends(get_current_user)): """Eigene Bewertung für einen Tierarzt (oder null).""" with db() as conn: row = conn.execute( "SELECT * FROM tierarzt_bewertungen WHERE tierarzt_id=? AND user_id=?", (tierarzt_id, user["id"]) ).fetchone() return dict(row) if row else None