- 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
138 lines
4.9 KiB
Python
138 lines
4.9 KiB
Python
"""BAN YARO — Gesundheitsdokumente (Befunde, Röntgen, Rezepte …)"""
|
|
|
|
import os
|
|
import uuid
|
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
|
from typing import Optional
|
|
from database import db
|
|
from auth import get_current_user
|
|
|
|
router = APIRouter()
|
|
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
|
|
|
|
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp", ".pdf"}
|
|
MAX_SIZE_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
|
|
ERLAUBTE_TYPEN = {"blutbild", "roentgen", "rezept", "impfausweis", "sonstiges"}
|
|
|
|
|
|
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/health-docs?dog_id=...
|
|
# ------------------------------------------------------------------
|
|
@router.get("")
|
|
async def list_docs(dog_id: int, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
rows = conn.execute(
|
|
"""SELECT hd.*, t.name AS vet_name
|
|
FROM health_documents hd
|
|
LEFT JOIN tieraerzte t ON t.id = hd.vet_id
|
|
WHERE hd.dog_id=?
|
|
ORDER BY hd.created_at DESC""",
|
|
(dog_id,)
|
|
).fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# POST /api/health-docs/upload (multipart/form-data)
|
|
# ------------------------------------------------------------------
|
|
@router.post("/upload", status_code=201)
|
|
async def upload_doc(
|
|
dog_id: int = Form(...),
|
|
typ: str = Form(...),
|
|
titel: str = Form(...),
|
|
beschreibung: Optional[str] = Form(None),
|
|
datum: Optional[str] = Form(None),
|
|
vet_id: Optional[int] = Form(None),
|
|
file: UploadFile = File(...),
|
|
user=Depends(get_current_user),
|
|
):
|
|
if typ not in ERLAUBTE_TYPEN:
|
|
raise HTTPException(400, f"Unbekannter Typ. Erlaubt: {', '.join(sorted(ERLAUBTE_TYPEN))}")
|
|
|
|
ext = os.path.splitext(file.filename or "")[1].lower()
|
|
if not ext:
|
|
ext = ".jpg"
|
|
if ext not in ALLOWED_EXTENSIONS:
|
|
raise HTTPException(400, "Nur JPG, PNG, WebP und PDF erlaubt.")
|
|
|
|
content = await file.read()
|
|
if len(content) > MAX_SIZE_BYTES:
|
|
raise HTTPException(413, "Datei zu groß. Maximal 10 MB erlaubt.")
|
|
|
|
with db() as conn:
|
|
_check_dog_owner(conn, dog_id, user["id"])
|
|
if vet_id:
|
|
vet = conn.execute("SELECT id FROM tieraerzte WHERE id=?", (vet_id,)).fetchone()
|
|
if not vet:
|
|
vet_id = None
|
|
|
|
# Datei speichern
|
|
dog_dir = os.path.join(MEDIA_DIR, "health_docs", str(dog_id))
|
|
os.makedirs(dog_dir, exist_ok=True)
|
|
filename = f"{uuid.uuid4().hex}{ext}"
|
|
filepath = os.path.join(dog_dir, filename)
|
|
with open(filepath, "wb") as f:
|
|
f.write(content)
|
|
|
|
file_url = f"/media/health_docs/{dog_id}/{filename}"
|
|
file_type = "pdf" if ext == ".pdf" else ext.lstrip(".")
|
|
|
|
with db() as conn:
|
|
conn.execute(
|
|
"""INSERT INTO health_documents
|
|
(dog_id, user_id, typ, titel, beschreibung, file_path, file_type, datum, vet_id)
|
|
VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
(dog_id, user["id"], typ, titel.strip(), beschreibung,
|
|
file_url, file_type, datum or None, vet_id)
|
|
)
|
|
row = conn.execute(
|
|
"""SELECT hd.*, t.name AS vet_name
|
|
FROM health_documents hd
|
|
LEFT JOIN tieraerzte t ON t.id = hd.vet_id
|
|
WHERE hd.id = last_insert_rowid()"""
|
|
).fetchone()
|
|
|
|
return dict(row)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# DELETE /api/health-docs/{id}
|
|
# ------------------------------------------------------------------
|
|
@router.delete("/{doc_id}", status_code=204)
|
|
async def delete_doc(doc_id: int, user=Depends(get_current_user)):
|
|
with db() as conn:
|
|
row = conn.execute(
|
|
"SELECT * FROM health_documents WHERE id=? AND user_id=?",
|
|
(doc_id, user["id"])
|
|
).fetchone()
|
|
if not row:
|
|
raise HTTPException(404, "Dokument nicht gefunden.")
|
|
|
|
# Datei löschen — file_path ist z.B. /media/health_docs/42/abc123.pdf
|
|
file_path = row["file_path"]
|
|
if file_path:
|
|
# /media/... → MEDIA_DIR/...
|
|
rel = file_path.lstrip("/")
|
|
if rel.startswith("media/"):
|
|
rel = rel[len("media/"):]
|
|
abs_path = os.path.join(MEDIA_DIR, rel)
|
|
if os.path.isfile(abs_path):
|
|
try:
|
|
os.remove(abs_path)
|
|
except OSError:
|
|
pass
|
|
|
|
conn.execute("DELETE FROM health_documents WHERE id=?", (doc_id,))
|
|
|
|
return None
|