""" Nutrition Tracking Endpoints for Mitai Jinkendo Handles nutrition data, FDDB CSV import, correlations, and weekly aggregates. """ import csv import io import uuid import logging from typing import Optional from datetime import datetime from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends from db import get_db, get_cursor, r2d from auth import require_auth, check_feature_access, increment_feature_usage from routers.profiles import get_pid from feature_logger import log_feature_usage router = APIRouter(prefix="/api/nutrition", tags=["nutrition"]) logger = logging.getLogger(__name__) # ── Helper ──────────────────────────────────────────────────────────────────── def _pf(s): """Parse float from string (handles comma decimal separator).""" try: return float(str(s).replace(',','.').strip()) except: return 0.0 # ── Endpoints ───────────────────────────────────────────────────────────────── @router.post("/import-csv") async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Import FDDB nutrition CSV.""" pid = get_pid(x_profile_id) # Phase 4: Check feature access and ENFORCE # Note: CSV import can create many entries - we check once before import access = check_feature_access(pid, 'nutrition_entries') log_feature_usage(pid, 'nutrition_entries', access, 'import_csv') if not access['allowed']: logger.warning( f"[FEATURE-LIMIT] User {pid} blocked: " f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})" ) raise HTTPException( status_code=403, detail=f"Limit erreicht: Du hast das Kontingent für Ernährungseinträge überschritten ({access['used']}/{access['limit']}). " f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset." ) raw = await file.read() try: text = raw.decode('utf-8') except: text = raw.decode('latin-1') if text.startswith('\ufeff'): text = text[1:] if not text.strip(): raise HTTPException(400,"Leere Datei") reader = csv.DictReader(io.StringIO(text), delimiter=';') days: dict = {} count = 0 for row in reader: rd = row.get('datum_tag_monat_jahr_stunde_minute','').strip().strip('"') if not rd: continue try: p = rd.split(' ')[0].split('.') iso = f"{p[2]}-{p[1]}-{p[0]}" except: continue days.setdefault(iso,{'kcal':0,'fat_g':0,'carbs_g':0,'protein_g':0}) days[iso]['kcal'] += _pf(row.get('kj',0))/4.184 days[iso]['fat_g'] += _pf(row.get('fett_g',0)) days[iso]['carbs_g'] += _pf(row.get('kh_g',0)) days[iso]['protein_g'] += _pf(row.get('protein_g',0)) count+=1 inserted=0 new_entries=0 with get_db() as conn: cur = get_cursor(conn) for iso,vals in days.items(): kcal=round(vals['kcal'],1); fat=round(vals['fat_g'],1) carbs=round(vals['carbs_g'],1); prot=round(vals['protein_g'],1) cur.execute("SELECT id FROM nutrition_log WHERE profile_id=%s AND date=%s",(pid,iso)) is_new = not cur.fetchone() if not is_new: # UPDATE existing cur.execute("UPDATE nutrition_log SET kcal=%s,protein_g=%s,fat_g=%s,carbs_g=%s WHERE profile_id=%s AND date=%s", (kcal,prot,fat,carbs,pid,iso)) else: # INSERT new cur.execute("INSERT INTO nutrition_log (id,profile_id,date,kcal,protein_g,fat_g,carbs_g,source,created) VALUES (%s,%s,%s,%s,%s,%s,%s,'csv',CURRENT_TIMESTAMP)", (str(uuid.uuid4()),pid,iso,kcal,prot,fat,carbs)) new_entries += 1 inserted+=1 # Phase 2: Increment usage counter for each new entry created for _ in range(new_entries): increment_feature_usage(pid, 'nutrition_entries') return {"rows_parsed":count,"days_imported":inserted,"new_entries":new_entries, "date_range":{"from":min(days) if days else None,"to":max(days) if days else None}} @router.post("") def create_nutrition(date: str, kcal: float, protein_g: float, fat_g: float, carbs_g: float, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Create or update nutrition entry for a specific date.""" pid = get_pid(x_profile_id) # Validate date format try: datetime.strptime(date, '%Y-%m-%d') except ValueError: raise HTTPException(400, "Ungültiges Datumsformat. Erwartet: YYYY-MM-DD") with get_db() as conn: cur = get_cursor(conn) # Check if entry exists cur.execute("SELECT id FROM nutrition_log WHERE profile_id=%s AND date=%s", (pid, date)) existing = cur.fetchone() if existing: # UPDATE existing entry cur.execute(""" UPDATE nutrition_log SET kcal=%s, protein_g=%s, fat_g=%s, carbs_g=%s, source='manual' WHERE id=%s AND profile_id=%s """, (round(kcal,1), round(protein_g,1), round(fat_g,1), round(carbs_g,1), existing['id'], pid)) return {"success": True, "mode": "updated", "id": existing['id']} else: # Phase 4: Check feature access before INSERT access = check_feature_access(pid, 'nutrition_entries') log_feature_usage(pid, 'nutrition_entries', access, 'create') if not access['allowed']: logger.warning( f"[FEATURE-LIMIT] User {pid} blocked: " f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})" ) raise HTTPException( status_code=403, detail=f"Limit erreicht: Du hast das Kontingent für Ernährungseinträge überschritten ({access['used']}/{access['limit']}). " f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset." ) # INSERT new entry new_id = str(uuid.uuid4()) cur.execute(""" INSERT INTO nutrition_log (id, profile_id, date, kcal, protein_g, fat_g, carbs_g, source, created) VALUES (%s, %s, %s, %s, %s, %s, %s, 'manual', CURRENT_TIMESTAMP) """, (new_id, pid, date, round(kcal,1), round(protein_g,1), round(fat_g,1), round(carbs_g,1))) # Phase 2: Increment usage counter increment_feature_usage(pid, 'nutrition_entries') return {"success": True, "mode": "created", "id": new_id} @router.get("") def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Get nutrition entries for current profile.""" pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute( "SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) return [r2d(r) for r in cur.fetchall()] @router.get("/by-date/{date}") def get_nutrition_by_date(date: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Get nutrition entry for a specific date.""" pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s AND date=%s", (pid, date)) row = cur.fetchone() return r2d(row) if row else None @router.get("/correlations") def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Get nutrition data correlated with weight and body fat.""" pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date",(pid,)) nutr={r['date']:r2d(r) for r in cur.fetchall()} cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date",(pid,)) wlog={r['date']:r['weight'] for r in cur.fetchall()} cur.execute("SELECT date,lean_mass,body_fat_pct FROM caliper_log WHERE profile_id=%s ORDER BY date",(pid,)) cals=sorted([r2d(r) for r in cur.fetchall()],key=lambda x:x['date']) all_dates=sorted(set(list(nutr)+list(wlog))) mi,last_cal,cal_by_date=0,{},{} for d in all_dates: while mi