- client_time: Browser-Lokalzeit bei allen Creates mitschicken (Tagebuch, Notizen, Forum, Verlorener Hund, Routen) — kein UTC-Versatz mehr bei Einträgen - Gewicht-Sync: health typ=gewicht schreibt dogs.gewicht_kg, einmalige Migration - Praxen: opening_hours + lat/lon/osm_id in tieraerzte-Tabelle, OSM-Nearby-Lookup, Öffnungszeiten in Karte und Detailansicht - KI-Gesundheitsbericht: alle 2 Wochen automatisch, ki_health_reports-Tabelle, Frontend-Banner mit Archiv (letzten 5 Berichte) - POI-Korrekturen: User schlägt Öffnungszeiten-Änderung vor, Moderatoren-Tab genehmigt/lehnt ab, user_edited-Flag schützt vor Overpass-Überschreibung - timeutils.py: safe_client_time() zentral für alle Routen
169 lines
6.1 KiB
Python
169 lines
6.1 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("")
|
||
async def list_tieraerzte(user=Depends(get_current_user)):
|
||
"""Alle Tierärzte des Users — aktive zuerst, dann inaktive."""
|
||
with db() as conn:
|
||
rows = conn.execute(
|
||
"SELECT * FROM tieraerzte WHERE user_id=? ORDER BY aktiv DESC, name",
|
||
(user["id"],)
|
||
).fetchall()
|
||
return [dict(r) for r in rows]
|
||
|
||
|
||
@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)
|