diff --git a/backend/routers/nutrition.py b/backend/routers/nutrition.py index 6738331..65d6777 100644 --- a/backend/routers/nutrition.py +++ b/backend/routers/nutrition.py @@ -99,6 +99,61 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona "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.""" @@ -110,6 +165,17 @@ def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=No 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.""" diff --git a/frontend/src/pages/NutritionPage.jsx b/frontend/src/pages/NutritionPage.jsx index 4a75ee9..fa0ef30 100644 --- a/frontend/src/pages/NutritionPage.jsx +++ b/frontend/src/pages/NutritionPage.jsx @@ -18,6 +18,179 @@ function rollingAvg(arr, key, window=7) { }) } +// ── Entry Form (Create/Update) ─────────────────────────────────────────────── +function EntryForm({ onSaved }) { + const [date, setDate] = useState(dayjs().format('YYYY-MM-DD')) + const [values, setValues] = useState({ kcal: '', protein_g: '', fat_g: '', carbs_g: '' }) + const [existingId, setExistingId] = useState(null) + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + + // Load data for selected date + useEffect(() => { + const load = async () => { + if (!date) return + setLoading(true) + setError(null) + try { + const data = await nutritionApi.getNutritionByDate(date) + if (data) { + setValues({ + kcal: data.kcal || '', + protein_g: data.protein_g || '', + fat_g: data.fat_g || '', + carbs_g: data.carbs_g || '' + }) + setExistingId(data.id) + } else { + setValues({ kcal: '', protein_g: '', fat_g: '', carbs_g: '' }) + setExistingId(null) + } + } catch(e) { + console.error('Failed to load entry:', e) + } finally { + setLoading(false) + } + } + load() + }, [date]) + + const handleSave = async () => { + if (!date || !values.kcal) { + setError('Datum und Kalorien sind Pflichtfelder') + return + } + + setSaving(true) + setError(null) + setSuccess(null) + try { + const result = await nutritionApi.createNutrition( + date, + parseFloat(values.kcal) || 0, + parseFloat(values.protein_g) || 0, + parseFloat(values.fat_g) || 0, + parseFloat(values.carbs_g) || 0 + ) + setSuccess(result.mode === 'created' ? 'Eintrag hinzugefügt' : 'Eintrag aktualisiert') + setTimeout(() => setSuccess(null), 3000) + onSaved() + } catch(e) { + if (e.message.includes('Limit erreicht')) { + setError(e.message) + } else { + setError('Speichern fehlgeschlagen: ' + e.message) + } + setTimeout(() => setError(null), 5000) + } finally { + setSaving(false) + } + } + + return ( +