Feature: Sprint31 — 9 Features merged (Streak, Ausgaben, KI-Tierarzt, Rückrufe, Adoption, Vet+Befunde, Hundepass, Playdate, Rassenerkennung)
- 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
This commit is contained in:
parent
031c6028ac
commit
742ad189e8
26 changed files with 5734 additions and 27 deletions
138
backend/routes/health_docs.py
Normal file
138
backend/routes/health_docs.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue