"""BAN YARO — Digitaler Hundepass""" import io import secrets from datetime import date, datetime, timedelta from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import StreamingResponse from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user router = APIRouter() # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class PassportMeta(BaseModel): blutgruppe: Optional[str] = None allergien: Optional[str] = None besonderheiten: Optional[str] = None class VaccinationCreate(BaseModel): krankheit: str datum: str naechste: Optional[str] = None tierarzt: Optional[str] = None charge_nr: Optional[str] = None class MedicationCreate(BaseModel): name: str dosierung: Optional[str] = None von: Optional[str] = None bis: Optional[str] = None notiz: Optional[str] = None # ------------------------------------------------------------------ # Hilfsfunktion: Eigentümer-Prüfung # ------------------------------------------------------------------ def _get_own_dog(conn, dog_id: int, user_id: int): 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.") return dog def _load_passport_data(conn, dog_id: int) -> dict: dog = conn.execute("SELECT * FROM dogs WHERE id=?", (dog_id,)).fetchone() if not dog: raise HTTPException(404, "Hund nicht gefunden.") meta = conn.execute( "SELECT * FROM dog_passport_meta WHERE dog_id=?", (dog_id,) ).fetchone() vaccinations = conn.execute( "SELECT * FROM vaccinations WHERE dog_id=? ORDER BY datum DESC", (dog_id,) ).fetchall() medications = conn.execute( "SELECT * FROM medications WHERE dog_id=? ORDER BY von DESC, id DESC", (dog_id,) ).fetchall() return { "dog": dict(dog), "meta": dict(meta) if meta else {}, "vaccinations": [dict(v) for v in vaccinations], "medications": [dict(m) for m in medications], } # ------------------------------------------------------------------ # GET /passport/{dog_id} — vollständige Passdaten # ------------------------------------------------------------------ @router.get("/{dog_id}") async def get_passport(dog_id: int, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) return _load_passport_data(conn, dog_id) # ------------------------------------------------------------------ # PUT /passport/{dog_id}/meta # ------------------------------------------------------------------ @router.put("/{dog_id}/meta") async def update_meta(dog_id: int, data: PassportMeta, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) conn.execute(""" INSERT INTO dog_passport_meta (dog_id, blutgruppe, allergien, besonderheiten, updated_at) VALUES (?, ?, ?, ?, datetime('now')) ON CONFLICT(dog_id) DO UPDATE SET blutgruppe = excluded.blutgruppe, allergien = excluded.allergien, besonderheiten = excluded.besonderheiten, updated_at = excluded.updated_at """, (dog_id, data.blutgruppe, data.allergien, data.besonderheiten)) return {"ok": True} # ------------------------------------------------------------------ # POST /passport/{dog_id}/vaccinations # ------------------------------------------------------------------ @router.post("/{dog_id}/vaccinations") async def add_vaccination(dog_id: int, data: VaccinationCreate, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) conn.execute(""" INSERT INTO vaccinations (dog_id, krankheit, datum, naechste, tierarzt, charge_nr) VALUES (?, ?, ?, ?, ?, ?) """, (dog_id, data.krankheit, data.datum, data.naechste, data.tierarzt, data.charge_nr)) row = conn.execute( "SELECT * FROM vaccinations WHERE dog_id=? ORDER BY id DESC LIMIT 1", (dog_id,) ).fetchone() return dict(row) # ------------------------------------------------------------------ # DELETE /passport/{dog_id}/vaccinations/{vacc_id} # ------------------------------------------------------------------ @router.delete("/{dog_id}/vaccinations/{vacc_id}", status_code=204) async def delete_vaccination(dog_id: int, vacc_id: int, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) conn.execute( "DELETE FROM vaccinations WHERE id=? AND dog_id=?", (vacc_id, dog_id) ) # ------------------------------------------------------------------ # POST /passport/{dog_id}/medications # ------------------------------------------------------------------ @router.post("/{dog_id}/medications") async def add_medication(dog_id: int, data: MedicationCreate, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) conn.execute(""" INSERT INTO medications (dog_id, name, dosierung, von, bis, notiz) VALUES (?, ?, ?, ?, ?, ?) """, (dog_id, data.name, data.dosierung, data.von, data.bis, data.notiz)) row = conn.execute( "SELECT * FROM medications WHERE dog_id=? ORDER BY id DESC LIMIT 1", (dog_id,) ).fetchone() return dict(row) # ------------------------------------------------------------------ # DELETE /passport/{dog_id}/medications/{med_id} # ------------------------------------------------------------------ @router.delete("/{dog_id}/medications/{med_id}", status_code=204) async def delete_medication(dog_id: int, med_id: int, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) conn.execute( "DELETE FROM medications WHERE id=? AND dog_id=?", (med_id, dog_id) ) # ------------------------------------------------------------------ # POST /passport/{dog_id}/share — Share-Token erstellen # ------------------------------------------------------------------ @router.post("/{dog_id}/share") async def create_share(dog_id: int, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) token = secrets.token_urlsafe(32) valid_until = (date.today() + timedelta(days=30)).isoformat() conn.execute(""" INSERT INTO passport_shares (dog_id, token, valid_until) VALUES (?, ?, ?) """, (dog_id, token, valid_until)) return { "token": token, "valid_until": valid_until, "url": f"/pass/{token}", } # ------------------------------------------------------------------ # GET /passport/share/{token} — öffentlicher Endpunkt (kein Auth) # ------------------------------------------------------------------ @router.get("/share/{token}") async def get_shared_passport(token: str): with db() as conn: share = conn.execute( "SELECT * FROM passport_shares WHERE token=?", (token,) ).fetchone() if not share: raise HTTPException(404, "Link nicht gefunden.") if share["valid_until"] < date.today().isoformat(): raise HTTPException(410, "Dieser Link ist abgelaufen.") return _load_passport_data(conn, share["dog_id"]) # ------------------------------------------------------------------ # GET /passport/{dog_id}/pdf — PDF generieren # ------------------------------------------------------------------ @router.get("/{dog_id}/pdf") async def download_pdf(dog_id: int, user=Depends(get_current_user)): with db() as conn: _get_own_dog(conn, dog_id, user["id"]) data = _load_passport_data(conn, dog_id) pdf_bytes = _generate_pdf(data) dog_name = data["dog"]["name"].replace(" ", "_") filename = f"Hundepass_{dog_name}.pdf" return StreamingResponse( io.BytesIO(pdf_bytes), media_type="application/pdf", headers={"Content-Disposition": f'attachment; filename="{filename}"'}, ) # ------------------------------------------------------------------ # PDF-Generierung mit fpdf2 # ------------------------------------------------------------------ def _generate_pdf(data: dict) -> bytes: try: from fpdf import FPDF except ImportError: raise HTTPException(500, "PDF-Bibliothek nicht verfügbar. Bitte fpdf2 installieren.") dog = data["dog"] meta = data["meta"] vaccs = data["vaccinations"] meds = data["medications"] # Datumsformatierung DE def _fmt_date(d): if not d: return "–" try: return datetime.strptime(d[:10], "%Y-%m-%d").strftime("%d.%m.%Y") except Exception: return d # Geschlecht geschlecht_map = {"m": "Rüde", "w": "Hündin"} pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=20) pdf.add_page() # ---- Header ---- pdf.set_fill_color(40, 167, 100) # Ban Yaro Grün pdf.rect(0, 0, 210, 38, style="F") pdf.set_text_color(255, 255, 255) pdf.set_font("Helvetica", style="B", size=20) pdf.set_y(8) pdf.cell(0, 10, "Ban Yaro", align="C", ln=True) pdf.set_font("Helvetica", size=11) pdf.cell(0, 8, f"Digitaler Hundepass — {dog['name']}", align="C", ln=True) pdf.set_font("Helvetica", size=8) pdf.cell(0, 6, f"Erstellt am {date.today().strftime('%d.%m.%Y')}", align="C", ln=True) pdf.set_text_color(30, 30, 30) pdf.set_y(46) # ---- Hundedaten ---- pdf.set_fill_color(245, 250, 247) pdf.set_draw_color(200, 200, 200) pdf.set_font("Helvetica", style="B", size=12) pdf.set_fill_color(235, 247, 240) pdf.cell(0, 8, " Hundeangaben", ln=True, fill=True, border="B") pdf.ln(3) def _info_row(label, value): pdf.set_font("Helvetica", style="B", size=9) pdf.cell(45, 6, label + ":", ln=False) pdf.set_font("Helvetica", size=9) pdf.cell(0, 6, str(value) if value else "–", ln=True) _info_row("Name", dog["name"]) _info_row("Rasse", dog.get("rasse") or "–") _info_row("Geburtstag", _fmt_date(dog.get("geburtstag"))) _info_row("Geschlecht", geschlecht_map.get(dog.get("geschlecht", ""), "–")) _info_row("Chip-Nr.", dog.get("chip_nr") or "–") if meta.get("blutgruppe"): _info_row("Blutgruppe", meta["blutgruppe"]) pdf.ln(5) # ---- Allergien & Besonderheiten ---- if meta.get("allergien") or meta.get("besonderheiten"): pdf.set_font("Helvetica", style="B", size=12) pdf.set_fill_color(235, 247, 240) pdf.cell(0, 8, " Allergien & Besonderheiten", ln=True, fill=True, border="B") pdf.ln(3) if meta.get("allergien"): pdf.set_font("Helvetica", style="B", size=9) pdf.cell(45, 6, "Allergien:", ln=False) pdf.set_font("Helvetica", size=9) pdf.multi_cell(0, 6, meta["allergien"]) if meta.get("besonderheiten"): pdf.set_font("Helvetica", style="B", size=9) pdf.cell(45, 6, "Besonderheiten:", ln=False) pdf.set_font("Helvetica", size=9) pdf.multi_cell(0, 6, meta["besonderheiten"]) pdf.ln(5) # ---- Impfungen ---- pdf.set_font("Helvetica", style="B", size=12) pdf.set_fill_color(235, 247, 240) pdf.cell(0, 8, " Impfungen", ln=True, fill=True, border="B") pdf.ln(3) if vaccs: # Tabellen-Header pdf.set_fill_color(220, 240, 228) pdf.set_font("Helvetica", style="B", size=8) pdf.cell(50, 6, "Krankheit", border=1, fill=True) pdf.cell(25, 6, "Datum", border=1, fill=True) pdf.cell(25, 6, "Nächste fällig", border=1, fill=True) pdf.cell(55, 6, "Tierarzt", border=1, fill=True) pdf.cell(35, 6, "Charge-Nr.", border=1, fill=True, ln=True) pdf.set_font("Helvetica", size=8) for i, v in enumerate(vaccs): fill = (i % 2 == 0) pdf.set_fill_color(248, 252, 250) if fill else pdf.set_fill_color(255, 255, 255) pdf.cell(50, 6, (v["krankheit"] or "")[:28], border=1, fill=fill) pdf.cell(25, 6, _fmt_date(v["datum"]), border=1, fill=fill) pdf.cell(25, 6, _fmt_date(v["naechste"]), border=1, fill=fill) pdf.cell(55, 6, (v["tierarzt"] or "–")[:32], border=1, fill=fill) pdf.cell(35, 6, (v["charge_nr"] or "–")[:20], border=1, fill=fill, ln=True) else: pdf.set_font("Helvetica", style="I", size=9) pdf.set_text_color(140, 140, 140) pdf.cell(0, 6, "Keine Impfungen eingetragen.", ln=True) pdf.set_text_color(30, 30, 30) pdf.ln(5) # ---- Medikamente ---- pdf.set_font("Helvetica", style="B", size=12) pdf.set_fill_color(235, 247, 240) pdf.cell(0, 8, " Medikamente", ln=True, fill=True, border="B") pdf.ln(3) if meds: pdf.set_fill_color(220, 240, 228) pdf.set_font("Helvetica", style="B", size=8) pdf.cell(55, 6, "Medikament", border=1, fill=True) pdf.cell(35, 6, "Dosierung", border=1, fill=True) pdf.cell(25, 6, "Von", border=1, fill=True) pdf.cell(25, 6, "Bis", border=1, fill=True) pdf.cell(50, 6, "Notiz", border=1, fill=True, ln=True) pdf.set_font("Helvetica", size=8) for i, m in enumerate(meds): fill = (i % 2 == 0) pdf.set_fill_color(248, 252, 250) if fill else pdf.set_fill_color(255, 255, 255) pdf.cell(55, 6, (m["name"] or "")[:32], border=1, fill=fill) pdf.cell(35, 6, (m["dosierung"] or "–")[:22], border=1, fill=fill) pdf.cell(25, 6, _fmt_date(m["von"]), border=1, fill=fill) bis = _fmt_date(m["bis"]) if m["bis"] else "dauerhaft" pdf.cell(25, 6, bis, border=1, fill=fill) pdf.cell(50, 6, (m["notiz"] or "–")[:30], border=1, fill=fill, ln=True) else: pdf.set_font("Helvetica", style="I", size=9) pdf.set_text_color(140, 140, 140) pdf.cell(0, 6, "Keine Medikamente eingetragen.", ln=True) pdf.set_text_color(30, 30, 30) # ---- Footer ---- pdf.set_y(-15) pdf.set_font("Helvetica", style="I", size=8) pdf.set_text_color(140, 140, 140) pdf.cell(0, 5, "Erstellt mit Ban Yaro — banyaro.app", align="C", ln=True) return bytes(pdf.output())