banyaro/backend/routes/tieraerzte.py

336 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""BAN YARO — Tierärzte Routes (user-level, nie löschen)"""
import math
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
router = APIRouter()
class TierarztCreate(BaseModel):
name: str
strasse: Optional[str] = None
plz: Optional[str] = None
ort: Optional[str] = None
telefon: Optional[str] = None
notfall_telefon: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
notizen: Optional[str] = None
ist_notfallpraxis: bool = False
opening_hours: Optional[str] = None
lat: Optional[float] = None
lon: Optional[float] = None
osm_id: Optional[str] = None
class BewertungCreate(BaseModel):
gesamt: int
wartezeit: Optional[int] = None
freundlichkeit: Optional[int] = None
kompetenz: Optional[int] = None
text: Optional[str] = None
class TierarztUpdate(BaseModel):
name: Optional[str] = None
strasse: Optional[str] = None
plz: Optional[str] = None
ort: Optional[str] = None
telefon: Optional[str] = None
notfall_telefon: Optional[str] = None
email: Optional[str] = None
website: Optional[str] = None
notizen: Optional[str] = None
ist_notfallpraxis: Optional[bool] = None
aktiv: Optional[bool] = None
opening_hours: Optional[str] = None
lat: Optional[float] = None
lon: Optional[float] = None
osm_id: Optional[str] = None
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"
"MoFr 08:0018:00 · Sa 09:0013: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