"""BAN YARO — Gassi-Treffen""" import math from datetime import date from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Optional, List from database import db from auth import get_current_user router = APIRouter() def _haversine(lat1, lon1, lat2, lon2): R = 6_371_000 p1, p2 = math.radians(lat1), math.radians(lat2) dp = math.radians(lat2 - lat1) dl = math.radians(lon2 - lon1) a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 return 2 * R * math.asin(math.sqrt(a)) # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class WalkCreate(BaseModel): titel: str datum: str # YYYY-MM-DD uhrzeit: str # HH:MM lat: float lon: float ort_name: Optional[str] = None max_teilnehmer: int = 10 beschreibung: Optional[str] = None class WalkUpdate(BaseModel): titel: Optional[str] = None datum: Optional[str] = None uhrzeit: Optional[str] = None lat: Optional[float] = None lon: Optional[float] = None ort_name: Optional[str] = None max_teilnehmer: Optional[int] = None beschreibung: Optional[str] = None class JoinRequest(BaseModel): dog_ids: List[int] = [] # leere Liste = ohne Hund (selten) # ------------------------------------------------------------------ # GET /api/walks — alle offenen Treffen (ab heute, optional Umkreis) # ------------------------------------------------------------------ @router.get("") async def list_walks( lat: Optional[float] = None, lon: Optional[float] = None, radius: int = 20000, alle: bool = False, # True → auch vergangene / stornierte ): today = date.today().isoformat() with db() as conn: q = """ SELECT w.*, u.name AS veranstalter_name, COUNT(DISTINCT wp.user_id) AS teilnehmer_count FROM walks w LEFT JOIN users u ON u.id = w.user_id LEFT JOIN walk_participants wp ON wp.walk_id = w.id WHERE w.status != 'storniert' """ if not alle: q += f" AND w.datum >= '{today}'" q += " GROUP BY w.id ORDER BY w.datum ASC, w.uhrzeit ASC" rows = conn.execute(q).fetchall() result = [dict(r) for r in rows] # Umkreis-Filter if lat is not None and lon is not None: result = [r for r in result if _haversine(lat, lon, r['lat'], r['lon']) <= radius] return result # ------------------------------------------------------------------ # POST /api/walks — Treffen erstellen # ------------------------------------------------------------------ @router.post("", status_code=201) async def create_walk(data: WalkCreate, user=Depends(get_current_user)): with db() as conn: cur = conn.execute(""" INSERT INTO walks (user_id, titel, datum, uhrzeit, lat, lon, ort_name, max_teilnehmer, beschreibung) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, (user['id'], data.titel, data.datum, data.uhrzeit, data.lat, data.lon, data.ort_name, data.max_teilnehmer, data.beschreibung)) row = conn.execute( "SELECT w.*, u.name AS veranstalter_name FROM walks w " "LEFT JOIN users u ON u.id = w.user_id WHERE w.id = ?", (cur.lastrowid,) ).fetchone() return {**dict(row), 'teilnehmer_count': 0} # ------------------------------------------------------------------ # GET /api/walks/{id} — Detail mit Teilnehmerliste # ------------------------------------------------------------------ @router.get("/{walk_id}") async def get_walk(walk_id: int): with db() as conn: walk = conn.execute( "SELECT w.*, u.name AS veranstalter_name FROM walks w " "LEFT JOIN users u ON u.id = w.user_id WHERE w.id = ?", (walk_id,) ).fetchone() if not walk: raise HTTPException(404, "Treffen nicht gefunden.") # Teilnehmer mit Hunden participants = conn.execute(""" SELECT wp.user_id, u.name AS user_name, GROUP_CONCAT(d.name, ', ') AS hunde FROM walk_participants wp JOIN users u ON u.id = wp.user_id LEFT JOIN walk_participant_dogs wpd ON wpd.walk_id = wp.walk_id AND wpd.user_id = wp.user_id LEFT JOIN dogs d ON d.id = wpd.dog_id WHERE wp.walk_id = ? GROUP BY wp.user_id """, (walk_id,)).fetchall() result = dict(walk) result['teilnehmer'] = [dict(p) for p in participants] result['teilnehmer_count'] = len(result['teilnehmer']) return result # ------------------------------------------------------------------ # PATCH /api/walks/{id} # ------------------------------------------------------------------ @router.patch("/{walk_id}") async def update_walk(walk_id: int, data: WalkUpdate, user=Depends(get_current_user)): with db() as conn: walk = conn.execute("SELECT * FROM walks WHERE id = ?", (walk_id,)).fetchone() if not walk: raise HTTPException(404, "Treffen nicht gefunden.") if walk['user_id'] != user['id']: raise HTTPException(403, "Nur der Veranstalter kann das Treffen bearbeiten.") updates = data.model_dump(exclude_none=True) if updates: cols = ', '.join(f"{k} = ?" for k in updates) conn.execute(f"UPDATE walks SET {cols} WHERE id = ?", [*updates.values(), walk_id]) row = conn.execute( "SELECT w.*, u.name AS veranstalter_name FROM walks w " "LEFT JOIN users u ON u.id = w.user_id WHERE w.id = ?", (walk_id,) ).fetchone() count = conn.execute( "SELECT COUNT(*) FROM walk_participants WHERE walk_id = ?", (walk_id,) ).fetchone()[0] return {**dict(row), 'teilnehmer_count': count} # ------------------------------------------------------------------ # DELETE /api/walks/{id} — stornieren # ------------------------------------------------------------------ @router.delete("/{walk_id}", status_code=204) async def cancel_walk(walk_id: int, user=Depends(get_current_user)): with db() as conn: walk = conn.execute("SELECT * FROM walks WHERE id = ?", (walk_id,)).fetchone() if not walk: raise HTTPException(404, "Treffen nicht gefunden.") if walk['user_id'] != user['id']: raise HTTPException(403, "Nur der Veranstalter kann das Treffen stornieren.") conn.execute("UPDATE walks SET status = 'storniert' WHERE id = ?", (walk_id,)) # ------------------------------------------------------------------ # POST /api/walks/{id}/join — beitreten # ------------------------------------------------------------------ @router.post("/{walk_id}/join") async def join_walk(walk_id: int, data: JoinRequest, user=Depends(get_current_user)): with db() as conn: walk = conn.execute("SELECT * FROM walks WHERE id = ?", (walk_id,)).fetchone() if not walk: raise HTTPException(404, "Treffen nicht gefunden.") if walk['status'] != 'offen': raise HTTPException(400, "Dieses Treffen ist nicht mehr offen.") # Bereits beigetreten? existing = conn.execute( "SELECT 1 FROM walk_participants WHERE walk_id = ? AND user_id = ?", (walk_id, user['id']) ).fetchone() if existing: raise HTTPException(409, "Du nimmst bereits teil.") # Platz frei? count = conn.execute( "SELECT COUNT(*) FROM walk_participants WHERE walk_id = ?", (walk_id,) ).fetchone()[0] if count >= walk['max_teilnehmer']: raise HTTPException(400, "Das Treffen ist bereits voll.") # Beitreten primary_dog = data.dog_ids[0] if data.dog_ids else None conn.execute( "INSERT INTO walk_participants (walk_id, user_id, dog_id) VALUES (?, ?, ?)", (walk_id, user['id'], primary_dog) ) # Hunde eintragen for dog_id in data.dog_ids: conn.execute( "INSERT OR IGNORE INTO walk_participant_dogs (walk_id, user_id, dog_id) VALUES (?, ?, ?)", (walk_id, user['id'], dog_id) ) new_count = count + 1 if new_count >= walk['max_teilnehmer']: conn.execute("UPDATE walks SET status = 'voll' WHERE id = ?", (walk_id,)) return {"status": "joined", "teilnehmer_count": new_count} # ------------------------------------------------------------------ # DELETE /api/walks/{id}/join — verlassen # ------------------------------------------------------------------ @router.delete("/{walk_id}/join", status_code=200) async def leave_walk(walk_id: int, user=Depends(get_current_user)): with db() as conn: walk = conn.execute("SELECT * FROM walks WHERE id = ?", (walk_id,)).fetchone() if not walk: raise HTTPException(404, "Treffen nicht gefunden.") if walk['user_id'] == user['id']: raise HTTPException(400, "Als Veranstalter kannst du nicht austreten — storniere das Treffen stattdessen.") conn.execute( "DELETE FROM walk_participants WHERE walk_id = ? AND user_id = ?", (walk_id, user['id']) ) conn.execute( "DELETE FROM walk_participant_dogs WHERE walk_id = ? AND user_id = ?", (walk_id, user['id']) ) # Status ggf. wieder auf offen setzen count = conn.execute( "SELECT COUNT(*) FROM walk_participants WHERE walk_id = ?", (walk_id,) ).fetchone()[0] if walk['status'] == 'voll': conn.execute("UPDATE walks SET status = 'offen' WHERE id = ?", (walk_id,)) return {"status": "left", "teilnehmer_count": count}