Neue Praxen-Tab in Gesundheit: Tierarzt-Stammdaten (Name, Adresse, Telefon, Notfall-Nr, E-Mail, Website, Notizen), Anruf- und Notfall-Schnellzugriff via tel:-Links, Soft-Delete (aktiv=0) für Praxiswechsel ohne Datenverlust. Tierarzt-Dropdown beim Eintragen von Tierarzt-Besuchen. SW-Cache → by-v7.
286 lines
10 KiB
Python
286 lines
10 KiB
Python
"""BAN YARO — Gesundheit & Impfpass Routes"""
|
|
|
|
import os, uuid
|
|
from datetime import date, datetime
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
from database import db
|
|
from auth import get_current_user
|
|
|
|
router = APIRouter()
|
|
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
|
|
|
# Erlaubte Typen
|
|
TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument"}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Schemas
|
|
# ------------------------------------------------------------------
|
|
class HealthCreate(BaseModel):
|
|
typ: str
|
|
bezeichnung: str
|
|
datum: str
|
|
naechstes: Optional[str] = None
|
|
notiz: Optional[str] = None
|
|
# Gewicht
|
|
wert: Optional[float] = None
|
|
einheit: Optional[str] = "kg"
|
|
# Impfung
|
|
charge_nr: Optional[str] = None
|
|
tierarzt_name: Optional[str] = None
|
|
# Tierarztbesuch
|
|
kosten: Optional[float] = None
|
|
diagnose: Optional[str] = None
|
|
# Medikament
|
|
dosierung: Optional[str] = None
|
|
haeufigkeit: Optional[str] = None
|
|
aktiv: Optional[int] = 1
|
|
bis_datum: Optional[str] = None
|
|
# Allergie
|
|
schweregrad: Optional[str] = None # leicht | mittel | schwer
|
|
reaktion: Optional[str] = None
|
|
erinnerung: Optional[int] = 1
|
|
# Tierarzt-Verknüpfung
|
|
tierarzt_id: Optional[int] = None
|
|
|
|
|
|
class HealthUpdate(BaseModel):
|
|
bezeichnung: Optional[str] = None
|
|
datum: Optional[str] = None
|
|
naechstes: Optional[str] = None
|
|
notiz: Optional[str] = None
|
|
wert: Optional[float] = None
|
|
einheit: Optional[str] = None
|
|
charge_nr: Optional[str] = None
|
|
tierarzt_name: Optional[str] = None
|
|
kosten: Optional[float] = None
|
|
diagnose: Optional[str] = None
|
|
dosierung: Optional[str] = None
|
|
haeufigkeit: Optional[str] = None
|
|
aktiv: Optional[int] = None
|
|
bis_datum: Optional[str] = None
|
|
schweregrad: Optional[str] = None
|
|
reaktion: Optional[str] = None
|
|
erinnerung: Optional[int] = None
|
|
tierarzt_id: Optional[int] = None
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# Hilfsfunktion: Zugriffscheck Dog → User
|
|
# ------------------------------------------------------------------
|
|
def _check_dog_owner(conn, dog_id: int, user_id: int):
|
|
dog = conn.execute(
|
|
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id)
|
|
).fetchone()
|
|
if not dog:
|
|
raise HTTPException(404, "Hund nicht gefunden.")
|
|
return dog
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# GET /api/dogs/{dog_id}/health
|
|
# ------------------------------------------------------------------
|
|
@router.get("/{dog_id}/health")
|
|
async def list_health(dog_id: int, typ: Optional[str] = None,
|
|
user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
if typ:
|
|
rows = conn.execute(
|
|
"SELECT * FROM health WHERE dog_id=? AND typ=? ORDER BY datum DESC",
|
|
(dog_id, typ)
|
|
).fetchall()
|
|
else:
|
|
rows = conn.execute(
|
|
"SELECT * FROM health WHERE dog_id=? ORDER BY datum DESC",
|
|
(dog_id,)
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/dogs/{dog_id}/health
|
|
# ------------------------------------------------------------------
|
|
@router.post("/{dog_id}/health", status_code=201)
|
|
async def create_health(dog_id: int, data: HealthCreate,
|
|
user=Depends(get_current_user)):
|
|
if data.typ not in TYPEN:
|
|
raise HTTPException(400, f"Unbekannter Typ. Erlaubt: {', '.join(TYPEN)}")
|
|
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
conn.execute(
|
|
"""INSERT INTO health
|
|
(dog_id, typ, bezeichnung, datum, naechstes, notiz,
|
|
wert, einheit, charge_nr, tierarzt_name, kosten, diagnose,
|
|
dosierung, haeufigkeit, aktiv, bis_datum,
|
|
schweregrad, reaktion, erinnerung, tierarzt_id)
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
(dog_id, data.typ, data.bezeichnung, data.datum, data.naechstes,
|
|
data.notiz, data.wert, data.einheit, data.charge_nr,
|
|
data.tierarzt_name, data.kosten, data.diagnose, data.dosierung,
|
|
data.haeufigkeit, data.aktiv, data.bis_datum,
|
|
data.schweregrad, data.reaktion, data.erinnerung, data.tierarzt_id)
|
|
)
|
|
row = conn.execute(
|
|
"SELECT * FROM health WHERE dog_id=? ORDER BY id DESC LIMIT 1",
|
|
(dog_id,)
|
|
).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# PATCH /api/dogs/{dog_id}/health/{id}
|
|
# ------------------------------------------------------------------
|
|
@router.patch("/{dog_id}/health/{entry_id}")
|
|
async def update_health(dog_id: int, entry_id: int, data: HealthUpdate,
|
|
user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
entry = conn.execute(
|
|
"SELECT * FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
|
).fetchone()
|
|
if not entry:
|
|
raise HTTPException(404, "Eintrag nicht gefunden.")
|
|
|
|
updates = {k: v for k, v in data.model_dump().items() if v is not None}
|
|
if not updates:
|
|
return dict(entry)
|
|
|
|
set_clause = ", ".join(f"{k}=?" for k in updates)
|
|
values = list(updates.values()) + [entry_id]
|
|
conn.execute(f"UPDATE health SET {set_clause} WHERE id=?", values)
|
|
row = conn.execute("SELECT * FROM health WHERE id=?", (entry_id,)).fetchone()
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/dogs/{dog_id}/health/{id}
|
|
# ------------------------------------------------------------------
|
|
@router.delete("/{dog_id}/health/{entry_id}", status_code=204)
|
|
async def delete_health(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
entry = conn.execute(
|
|
"SELECT id FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
|
).fetchone()
|
|
if not entry:
|
|
raise HTTPException(404, "Eintrag nicht gefunden.")
|
|
conn.execute("DELETE FROM health WHERE id=?", (entry_id,))
|
|
return None
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/dogs/{dog_id}/health/{id}/dokument — Datei-Upload
|
|
# ------------------------------------------------------------------
|
|
@router.post("/{dog_id}/health/{entry_id}/dokument")
|
|
async def upload_dokument(
|
|
dog_id: int,
|
|
entry_id: int,
|
|
file: UploadFile = File(...),
|
|
user=Depends(get_current_user),
|
|
):
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
entry = conn.execute(
|
|
"SELECT id FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
|
).fetchone()
|
|
if not entry:
|
|
raise HTTPException(404, "Eintrag nicht gefunden.")
|
|
|
|
ext = os.path.splitext(file.filename or "")[1].lower() or ".jpg"
|
|
if ext not in {".jpg", ".jpeg", ".png", ".pdf", ".webp"}:
|
|
raise HTTPException(400, "Nur JPG, PNG, WebP und PDF erlaubt.")
|
|
|
|
filename = f"health_{entry_id}_{uuid.uuid4().hex[:8]}{ext}"
|
|
path = os.path.join(MEDIA_DIR, "health", filename)
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
|
|
with open(path, "wb") as f:
|
|
f.write(await file.read())
|
|
|
|
datei_url = f"/media/health/{filename}"
|
|
datei_typ = "pdf" if ext == ".pdf" else "image"
|
|
|
|
with db() as conn:
|
|
conn.execute(
|
|
"UPDATE health SET datei_url=?, datei_typ=? WHERE id=?",
|
|
(datei_url, datei_typ, entry_id)
|
|
)
|
|
|
|
return {"datei_url": datei_url, "datei_typ": datei_typ}
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/dogs/{dog_id}/health/symptom-check — KI-Symptomprüfung
|
|
# ------------------------------------------------------------------
|
|
class SymptomCheckRequest(BaseModel):
|
|
symptoms: str
|
|
|
|
|
|
@router.post("/{dog_id}/health/symptom-check")
|
|
async def symptom_check(dog_id: int, data: SymptomCheckRequest,
|
|
user=Depends(get_current_user)):
|
|
from ki import symptom_check as ki_symptom_check, KIUnavailableError
|
|
|
|
with db() as conn:
|
|
dog = conn.execute(
|
|
"SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
|
|
).fetchone()
|
|
if not dog:
|
|
raise HTTPException(404, "Hund nicht gefunden.")
|
|
|
|
dog_info = dict(dog)
|
|
if dog_info.get("geburtstag"):
|
|
try:
|
|
from datetime import date
|
|
geb = date.fromisoformat(dog_info["geburtstag"])
|
|
dog_info["alter_jahre"] = round((date.today() - geb).days / 365.25, 1)
|
|
except Exception:
|
|
dog_info["alter_jahre"] = "unbekannt"
|
|
|
|
try:
|
|
result = await ki_symptom_check(
|
|
symptoms=data.symptoms,
|
|
dog_info=dog_info,
|
|
user_is_premium=bool(user.get("is_premium")),
|
|
)
|
|
return result
|
|
except KIUnavailableError as e:
|
|
raise HTTPException(503, str(e))
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/dogs/{dog_id}/health/ki-zusammenfassung
|
|
# ------------------------------------------------------------------
|
|
@router.post("/{dog_id}/health/ki-zusammenfassung")
|
|
async def ki_zusammenfassung(dog_id: int, user=Depends(get_current_user)):
|
|
from ki import health_summary, KIUnavailableError, KIPremiumRequired
|
|
|
|
with db() as conn:
|
|
dog = conn.execute(
|
|
"SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
|
|
).fetchone()
|
|
if not dog:
|
|
raise HTTPException(404, "Hund nicht gefunden.")
|
|
|
|
rows = conn.execute(
|
|
"SELECT * FROM health WHERE dog_id=? ORDER BY datum DESC",
|
|
(dog_id,)
|
|
).fetchall()
|
|
|
|
health_data = [dict(r) for r in rows]
|
|
|
|
try:
|
|
result = await health_summary(
|
|
health_data=health_data,
|
|
dog_info=dict(dog),
|
|
user_is_premium=bool(user.get("is_premium")),
|
|
)
|
|
return {"zusammenfassung": result}
|
|
except KIPremiumRequired as e:
|
|
raise HTTPException(402, str(e))
|
|
except KIUnavailableError as e:
|
|
raise HTTPException(503, str(e))
|