319 lines
11 KiB
Python
319 lines
11 KiB
Python
from datetime import date
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.auth.deps import get_current_user
|
|
from app.db.base import get_db
|
|
from app.db.models import User
|
|
from app.fitness.service import FitnessService
|
|
from app.fitness.structuring import structure_meal, structure_workout
|
|
from app.integrations.openfoodfacts import OpenFoodFactsClient
|
|
from app.integrations.wger import WgerClient
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
sex: str | None = None
|
|
age: int | None = None
|
|
height_cm: float | None = None
|
|
weight_kg: float | None = None
|
|
goal: str | None = None
|
|
target_weight_kg: float | None = None
|
|
neat_base_kcal: float | None = Field(default=None, ge=200, le=300)
|
|
|
|
|
|
class MealCreate(BaseModel):
|
|
text: str = Field(min_length=1)
|
|
meal_type: str | None = None
|
|
|
|
|
|
class WaterCreate(BaseModel):
|
|
amount_ml: int = Field(gt=0)
|
|
|
|
|
|
class WeightCreate(BaseModel):
|
|
weight_kg: float = Field(gt=0)
|
|
body_fat_pct: float | None = None
|
|
chest_cm: float | None = None
|
|
waist_cm: float | None = None
|
|
neck_cm: float | None = None
|
|
hip_cm: float | None = None
|
|
notes: str = ""
|
|
day: str | None = None
|
|
days_ago: int | None = Field(default=None, ge=0, le=90)
|
|
recorded_at: str | None = None
|
|
|
|
|
|
class BodyCompositionCalc(BaseModel):
|
|
weight_kg: float | None = None
|
|
height_cm: float | None = None
|
|
sex: str | None = None
|
|
neck_cm: float | None = None
|
|
waist_cm: float | None = None
|
|
hip_cm: float | None = None
|
|
body_fat_pct: float | None = None
|
|
|
|
|
|
class StepsCreate(BaseModel):
|
|
steps: int = Field(ge=0)
|
|
active_calories: float | None = None
|
|
notes: str = ""
|
|
day: str | None = None
|
|
days_ago: int | None = Field(default=None, ge=0, le=90)
|
|
logged_at: str | None = None
|
|
|
|
|
|
class WorkoutCreate(BaseModel):
|
|
text: str = Field(min_length=1)
|
|
day: str | None = None
|
|
days_ago: int | None = Field(default=None, ge=0, le=90)
|
|
logged_at: str | None = None
|
|
|
|
|
|
class ReminderUpdate(BaseModel):
|
|
enabled: bool | None = None
|
|
hour: int | None = Field(default=None, ge=0, le=23)
|
|
minute: int | None = Field(default=None, ge=0, le=59)
|
|
interval_hours: int | None = Field(default=None, ge=1, le=12)
|
|
|
|
|
|
@router.get("/fitness")
|
|
def get_snapshot(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
|
return FitnessService(db, user.id).snapshot()
|
|
|
|
|
|
@router.get("/fitness/summary")
|
|
def get_summary(
|
|
day: str | None = None,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
d = date.fromisoformat(day) if day else None
|
|
return FitnessService(db, user.id).get_daily_summary(d)
|
|
|
|
|
|
@router.get("/fitness/workout-stats")
|
|
def get_workout_stats(
|
|
days: int = 7,
|
|
end: str | None = None,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
end_day = date.fromisoformat(end) if end else None
|
|
return FitnessService(db, user.id).get_workout_stats(days=days, end_day=end_day)
|
|
|
|
|
|
@router.get("/fitness/history")
|
|
def get_history(
|
|
days: int = 7,
|
|
end: str | None = None,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
end_day = date.fromisoformat(end) if end else None
|
|
return FitnessService(db, user.id).get_history(days=days, end_day=end_day)
|
|
|
|
|
|
@router.get("/fitness/charts")
|
|
def get_charts(
|
|
weeks: int = 52,
|
|
trend: bool = True,
|
|
end: str | None = None,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
end_day = date.fromisoformat(end) if end else None
|
|
return FitnessService(db, user.id).get_charts(weeks=weeks, trend=trend, end_day=end_day)
|
|
|
|
|
|
@router.get("/fitness/profile")
|
|
def get_profile(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, Any]:
|
|
profile = FitnessService(db, user.id).get_profile()
|
|
return profile or {"configured": False}
|
|
|
|
|
|
@router.put("/fitness/profile")
|
|
def update_profile(
|
|
payload: ProfileUpdate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
return FitnessService(db, user.id).set_profile(payload.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/fitness/profile/calc")
|
|
def calc_targets(
|
|
payload: ProfileUpdate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
params = payload.model_dump(exclude_none=True)
|
|
if not params:
|
|
raise HTTPException(status_code=400, detail="No parameters")
|
|
return FitnessService(db, user.id).calc_targets(params)
|
|
|
|
|
|
@router.post("/fitness/meals")
|
|
async def create_meal(
|
|
payload: MealCreate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
service = FitnessService(db, user.id)
|
|
try:
|
|
structured = await structure_meal(payload.text)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
return service.log_meal(
|
|
description=structured.get("description") or payload.text,
|
|
meal_type=payload.meal_type or structured.get("meal_type") or "snack",
|
|
calories=float(structured.get("calories") or 0),
|
|
protein_g=float(structured.get("protein_g") or 0),
|
|
fat_g=float(structured.get("fat_g") or 0),
|
|
carbs_g=float(structured.get("carbs_g") or 0),
|
|
source="llm",
|
|
estimated=bool(structured.get("estimated", True)),
|
|
)
|
|
|
|
|
|
@router.post("/fitness/water")
|
|
def create_water(
|
|
payload: WaterCreate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
return FitnessService(db, user.id).log_water(payload.amount_ml)
|
|
|
|
|
|
@router.post("/fitness/weight")
|
|
def create_weight(
|
|
payload: WeightCreate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
day = date.fromisoformat(payload.day) if payload.day else None
|
|
return FitnessService(db, user.id).log_weight(
|
|
payload.weight_kg,
|
|
body_fat_pct=payload.body_fat_pct,
|
|
chest_cm=payload.chest_cm,
|
|
waist_cm=payload.waist_cm,
|
|
neck_cm=payload.neck_cm,
|
|
hip_cm=payload.hip_cm,
|
|
notes=payload.notes,
|
|
recorded_at=payload.recorded_at,
|
|
day=day,
|
|
days_ago=payload.days_ago,
|
|
)
|
|
|
|
|
|
@router.post("/fitness/body-composition/calc")
|
|
def calc_body_composition(
|
|
payload: BodyCompositionCalc,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
return FitnessService(db, user.id).calc_body_composition(payload.model_dump(exclude_none=True))
|
|
|
|
|
|
@router.post("/fitness/steps")
|
|
def create_steps(
|
|
payload: StepsCreate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
day = date.fromisoformat(payload.day) if payload.day else None
|
|
return FitnessService(db, user.id).log_steps(
|
|
payload.steps,
|
|
active_calories=payload.active_calories,
|
|
notes=payload.notes,
|
|
day=day,
|
|
days_ago=payload.days_ago,
|
|
logged_at=payload.logged_at,
|
|
)
|
|
|
|
|
|
@router.delete("/fitness/steps/{log_id}")
|
|
def delete_steps(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
|
if not FitnessService(db, user.id).delete_step_log(log_id):
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.post("/fitness/workouts")
|
|
async def create_workout(
|
|
payload: WorkoutCreate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
service = FitnessService(db, user.id)
|
|
try:
|
|
structured = await structure_workout(payload.text)
|
|
except Exception as exc:
|
|
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
day = date.fromisoformat(payload.day) if payload.day else None
|
|
return service.log_workout(
|
|
title=structured.get("title") or "Тренировка",
|
|
notes=structured.get("notes") or payload.text,
|
|
duration_min=structured.get("duration_min"),
|
|
exercises=structured.get("exercises"),
|
|
active_calories=structured.get("active_calories"),
|
|
total_calories=structured.get("total_calories"),
|
|
steps=structured.get("steps"),
|
|
activity_type=structured.get("activity_type"),
|
|
met=structured.get("met"),
|
|
day=day,
|
|
days_ago=payload.days_ago,
|
|
logged_at=payload.logged_at,
|
|
)
|
|
|
|
|
|
@router.get("/fitness/body-metrics")
|
|
def list_metrics(
|
|
limit: int = 30,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> list[dict[str, Any]]:
|
|
return FitnessService(db, user.id).list_body_metrics(limit=limit)
|
|
|
|
|
|
@router.delete("/fitness/meals/{log_id}")
|
|
def delete_meal(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
|
if not FitnessService(db, user.id).delete_food_log(log_id):
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/fitness/water/{log_id}")
|
|
def delete_water(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
|
if not FitnessService(db, user.id).delete_water_log(log_id):
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.delete("/fitness/workouts/{log_id}")
|
|
def delete_workout(log_id: int, db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> dict[str, bool]:
|
|
if not FitnessService(db, user.id).delete_workout_log(log_id):
|
|
raise HTTPException(status_code=404, detail="Not found")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/fitness/reminders")
|
|
def list_reminders(db: Session = Depends(get_db), user: User = Depends(get_current_user)) -> list[dict[str, Any]]:
|
|
return FitnessService(db, user.id).list_reminders()
|
|
|
|
|
|
@router.put("/fitness/reminders/{kind}")
|
|
def update_reminder(
|
|
kind: str,
|
|
payload: ReminderUpdate,
|
|
db: Session = Depends(get_db), user: User = Depends(get_current_user),
|
|
) -> dict[str, Any]:
|
|
return FitnessService(db, user.id).set_reminder(
|
|
kind,
|
|
enabled=payload.enabled,
|
|
hour=payload.hour,
|
|
minute=payload.minute,
|
|
interval_hours=payload.interval_hours,
|
|
)
|
|
|
|
|
|
@router.get("/fitness/lookup/food")
|
|
def lookup_food(q: str, limit: int = 5) -> list[dict[str, Any]]:
|
|
return OpenFoodFactsClient().search(q, limit=limit)
|
|
|
|
|
|
@router.get("/fitness/lookup/exercise")
|
|
def lookup_exercise(q: str, limit: int = 8) -> list[dict[str, Any]]:
|
|
return WgerClient().search_exercises(q, limit=limit)
|