"""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 from media_utils import safe_media_path router = APIRouter() MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"} # Erlaubte Typen TYPEN = {"impfung", "entwurmung", "tierarzt", "medikament", "gewicht", "allergie", "dokument", "laeufigkeit"} # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class HealthCreate(BaseModel): typ: str bezeichnung: Optional[str] = None 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 intervall_tage: Optional[int] = None # Wiederkehrend alle X Tage # 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 intervall_tage: Optional[int] = None tierarzt_id: Optional[int] = None # ------------------------------------------------------------------ # Hilfsfunktionen # ------------------------------------------------------------------ def _sync_gewicht(conn, dog_id: int): """Aktualisiert dogs.gewicht_kg auf den neuesten Gewichtseintrag (nach datum).""" conn.execute( """UPDATE dogs SET gewicht_kg = ( SELECT wert FROM health WHERE dog_id=? AND typ='gewicht' AND wert IS NOT NULL ORDER BY datum DESC, id DESC LIMIT 1 ) WHERE id=?""", (dog_id, dog_id) ) 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 def _fetch_media_items(conn, entry_ids: list) -> dict: """Gibt {health_id: [{id, url, media_type}, ...]} zurück.""" if not entry_ids: return {} ph = ",".join("?" * len(entry_ids)) rows = conn.execute( f"SELECT id, health_id, url, media_type FROM health_media " f"WHERE health_id IN ({ph}) ORDER BY health_id, sort_order", entry_ids ).fetchall() result = {} for r in rows: result.setdefault(r["health_id"], []).append({ "id": r["id"], "url": r["url"], "media_type": r["media_type"] }) return result def _entry_with_media(row, media_map: dict) -> dict: e = dict(row) e["media_items"] = media_map.get(e["id"], []) return e # ------------------------------------------------------------------ # 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() ids = [r["id"] for r in rows] media_map = _fetch_media_items(conn, ids) return [_entry_with_media(r, media_map) 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() media_map = _fetch_media_items(conn, [row["id"]]) if data.typ == 'gewicht': _sync_gewicht(conn, dog_id) return _entry_with_media(row, media_map) # ------------------------------------------------------------------ # 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() media_map = _fetch_media_items(conn, [entry_id]) if row["typ"] == 'gewicht': _sync_gewicht(conn, dog_id) return _entry_with_media(row, media_map) # ------------------------------------------------------------------ # 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, typ FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id) ).fetchone() if not entry: raise HTTPException(404, "Eintrag nicht gefunden.") was_gewicht = entry["typ"] == 'gewicht' conn.execute("DELETE FROM health WHERE id=?", (entry_id,)) if was_gewicht: _sync_gewicht(conn, dog_id) return None # ------------------------------------------------------------------ # DELETE /api/dogs/{dog_id}/health/{id}/dokument — Datei löschen # ------------------------------------------------------------------ @router.delete("/{dog_id}/health/{entry_id}/dokument") async def delete_dokument(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 datei_url FROM health WHERE id=? AND dog_id=?", (entry_id, dog_id) ).fetchone() if not entry: raise HTTPException(404, "Eintrag nicht gefunden.") datei_url = entry["datei_url"] if datei_url: path = safe_media_path(MEDIA_DIR, datei_url) if path and os.path.isfile(path): os.remove(path) conn.execute( "UPDATE health SET datei_url=NULL, datei_typ=NULL WHERE id=?", (entry_id,) ) return {"ok": True} # ------------------------------------------------------------------ # 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/{entry_id}/media — Datei-Upload (Multi) # ------------------------------------------------------------------ @router.post("/{dog_id}/health/{entry_id}/media") async def upload_media( 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 ALLOWED_EXTENSIONS: 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()) media_url = f"/media/health/{filename}" media_type = "pdf" if ext == ".pdf" else "image" with db() as conn: max_order = conn.execute( "SELECT COALESCE(MAX(sort_order), -1) FROM health_media WHERE health_id=?", (entry_id,) ).fetchone()[0] conn.execute( "INSERT INTO health_media (health_id, url, media_type, sort_order) VALUES (?,?,?,?)", (entry_id, media_url, media_type, max_order + 1) ) new_id = conn.execute( "SELECT id FROM health_media WHERE health_id=? ORDER BY id DESC LIMIT 1", (entry_id,) ).fetchone()["id"] return {"id": new_id, "url": media_url, "media_type": media_type, "sort_order": max_order + 1} # ------------------------------------------------------------------ # DELETE /api/dogs/{dog_id}/health/{entry_id}/media/{media_id} # ------------------------------------------------------------------ @router.delete("/{dog_id}/health/{entry_id}/media/{media_id}", status_code=204) async def delete_media_item(dog_id: int, entry_id: int, media_id: int, user=Depends(get_current_user)): with db() as conn: _check_dog_owner(conn, dog_id, user["id"]) row = conn.execute( "SELECT hm.id, hm.url FROM health_media hm " "JOIN health h ON h.id = hm.health_id " "WHERE hm.id=? AND hm.health_id=? AND h.dog_id=?", (media_id, entry_id, dog_id) ).fetchone() if not row: raise HTTPException(404, "Medium nicht gefunden.") file_path = safe_media_path(MEDIA_DIR, row["url"]) if file_path: try: os.remove(file_path) except OSError: pass conn.execute("DELETE FROM health_media WHERE id=?", (media_id,)) # ------------------------------------------------------------------ # GET /api/dogs/{dog_id}/health/gewicht — Gewichtsverlauf # ------------------------------------------------------------------ @router.get("/{dog_id}/health/gewicht") async def list_gewicht(dog_id: int, user=Depends(get_current_user)): with db() as conn: _check_dog_owner(conn, dog_id, user["id"]) rows = conn.execute( """SELECT datum, wert AS gewicht FROM health WHERE dog_id=? AND typ='gewicht' AND wert IS NOT NULL ORDER BY datum ASC""", (dog_id,) ).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # 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")), user_id=user["id"], ) 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")), user_id=user["id"], ) return {"zusammenfassung": result} except KIPremiumRequired as e: raise HTTPException(402, str(e)) except KIUnavailableError as e: raise HTTPException(503, str(e)) # ------------------------------------------------------------------ # GET /api/dogs/{dog_id}/health/ki-berichte # ------------------------------------------------------------------ @router.get("/{dog_id}/health/ki-berichte") async def list_ki_berichte(dog_id: int, user=Depends(get_current_user)): with db() as conn: _check_dog_owner(conn, dog_id, user["id"]) rows = conn.execute( """SELECT id, bericht, erstellt_at FROM ki_health_reports WHERE dog_id=? ORDER BY erstellt_at DESC LIMIT 5""", (dog_id,) ).fetchall() return [dict(r) for r in rows]