- Trainings-Streak: streak.py, DB training_streaks, Scheduler 19:00, Widget in welcome.js, Ping in uebungen.js
- Ausgaben-Tracker: expenses.py, expenses.js, DB expenses-Tabelle
- KI-Tierarztfragen: ki.py /tierarzt, health.js Button+Modal, DB ki_tierarzt_log
- Rückruf-Alarm: recalls.py, recalls.js, DB feed_recalls, Scheduler 08:00 RASFF
- Adoption: adoption.py, adoption.js, DB adoption_cache
- Tierarzt-Favorit + Befunde: tieraerzte.py /my-favorite+/favorite, health_docs.py, health.js, api.js, DB favorite_vets+health_documents
- Digitaler Hundepass: passport.py, dog-profile.js, main.py /pass/{token}, DB vaccinations+medications+dog_passport_meta+passport_shares, requirements.txt fpdf2
- Playdate-Matching: playdate.py, playdate.js, DB playdate_listings+playdate_requests
- Rassen-Erkennung: ki.py /rasse-erkennung (Claude Vision), dog-profile.js+wiki.js, CSS .rasse-result-card, DB ki_rasse_log
222 lines
7.8 KiB
Python
222 lines
7.8 KiB
Python
"""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 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"
|
||
→ "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)
|