From a2ec853bbeb3917b4e130650817333f3a3ac6ab5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 06:42:39 +0000 Subject: [PATCH] =?UTF-8?q?Agregar=20planificaci=C3=B3n=20completa=20de=20?= =?UTF-8?q?app=20de=20seguimiento=20nutricional?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plan detallado con arquitectura, funcionalidades y roadmap - Estructura de directorios y esquema de base de datos - Ejemplos de código backend (Python/FastAPI) - Ejemplos de código frontend (React/TypeScript) - Guía completa de setup e instalación - README ejecutivo con resumen del proyecto El proyecto incluye: - Tracking de alimentación por fotos/descripción/manual - Análisis con IA usando GPT-4 Vision - Cálculo de calorías y macronutrientes (TMB/TDEE) - Generador de planes semanales personalizados - Objetivos: pérdida de peso, detox, ganancia muscular - Stack: FastAPI + React + PostgreSQL + OpenAI --- NUTRITION_APP_PLAN.md | 554 ++++++++++++++++ NUTRITION_APP_STRUCTURE.md | 542 +++++++++++++++ NUTRITION_CODE_EXAMPLES.md | 1143 ++++++++++++++++++++++++++++++++ NUTRITION_FRONTEND_EXAMPLES.md | 1036 +++++++++++++++++++++++++++++ NUTRITION_README.md | 441 ++++++++++++ NUTRITION_SETUP_GUIDE.md | 876 ++++++++++++++++++++++++ 6 files changed, 4592 insertions(+) create mode 100644 NUTRITION_APP_PLAN.md create mode 100644 NUTRITION_APP_STRUCTURE.md create mode 100644 NUTRITION_CODE_EXAMPLES.md create mode 100644 NUTRITION_FRONTEND_EXAMPLES.md create mode 100644 NUTRITION_README.md create mode 100644 NUTRITION_SETUP_GUIDE.md diff --git a/NUTRITION_APP_PLAN.md b/NUTRITION_APP_PLAN.md new file mode 100644 index 0000000..52b735f --- /dev/null +++ b/NUTRITION_APP_PLAN.md @@ -0,0 +1,554 @@ +# 🥗 Plan de Aplicación de Seguimiento Nutricional + +## 📋 Resumen Ejecutivo + +Aplicación de tracking alimentario que permite a los usuarios: +- Registrar comidas mediante **fotos** o **descripciones de recetas** +- Calcular automáticamente **calorías y macronutrientes** +- Recibir **planes semanales personalizados** según objetivos +- Hacer seguimiento de progreso hacia metas de salud + +--- + +## 🎯 Objetivos y Funcionalidades + +### Objetivos del Usuario +1. **Pérdida de peso** (déficit calórico controlado) +2. **Detox** (alimentos limpios, antiinflamatorios) +3. **Quema de grasa basal** (optimización metabólica) +4. **Mantenimiento** (equilibrio nutricional) +5. **Ganancia muscular** (superávit + proteína) + +### Funcionalidades Principales + +#### 1. Perfil de Usuario +- **Datos personales**: edad, sexo, peso, altura +- **Nivel de actividad**: + - Sedentario (poco o ningún ejercicio) + - Ligero (ejercicio 1-3 días/semana) + - Moderado (ejercicio 3-5 días/semana) + - Activo (ejercicio 6-7 días/semana) + - Muy activo (ejercicio intenso + trabajo físico) +- **Frecuencia de caminatas** (pasos diarios, minutos) +- **Objetivo actual** (pérdida de peso, detox, etc.) + +#### 2. Registro de Comidas +- **Por foto**: Analizar imagen con IA de visión por computadora +- **Por descripción**: Parser de recetas en lenguaje natural +- **Manual**: Búsqueda en base de datos de alimentos +- **Horarios**: Desayuno, almuerzo, merienda, cena, snacks + +#### 3. Análisis Nutricional +- **Calorías totales** por comida y día +- **Macronutrientes**: proteínas, carbohidratos, grasas +- **Micronutrientes**: vitaminas, minerales (opcional) +- **Fibra, azúcar, sodio** +- **Índice glucémico** estimado + +#### 4. Calculadora Metabólica +- **TMB** (Tasa Metabólica Basal) - Ecuación Mifflin-St Jeor +- **TDEE** (Total Daily Energy Expenditure) +- **Calorías objetivo** según meta +- **Distribución de macros** personalizada + +#### 5. Generador de Planes Semanales +- **Sugerencias de comidas** basadas en: + - Calorías objetivo + - Preferencias alimentarias + - Alergias/intolerancias + - Presupuesto + - Tiempo de preparación +- **Lista de compras** automática +- **Recetas detalladas** con pasos + +#### 6. Dashboard y Reportes +- **Gráficos de progreso** (peso, calorías, macros) +- **Streak tracking** (días consecutivos) +- **Insights y recomendaciones** +- **Comparativa semanal/mensual** + +--- + +## 🏗️ Arquitectura Técnica + +### Stack Tecnológico Propuesto + +#### Backend +``` +- Framework: FastAPI (Python 3.10+) +- Base de datos: PostgreSQL + Redis (caché) +- ORM: SQLAlchemy +- Autenticación: JWT (JSON Web Tokens) +- IA/ML: OpenAI GPT-4 Vision API + Hugging Face +- Hosting: Railway / Render / AWS Lambda +``` + +#### Frontend +``` +- Framework: React + TypeScript (Web) +- Mobile: React Native (iOS/Android) +- UI Library: Tailwind CSS + shadcn/ui +- Estado: Zustand / React Query +- Gráficos: Recharts / Chart.js +- Hosting: Vercel / Netlify +``` + +#### APIs y Servicios Externos +``` +- Visión por computadora: OpenAI GPT-4 Vision +- Base de datos de alimentos: USDA FoodData Central API (gratis) +- Alternativa: Nutritionix API, Spoonacular API +- OCR para recetas: Google Cloud Vision / Tesseract +``` + +--- + +## 📊 Modelo de Datos + +### 1. Tabla: `users` +```sql +- id (UUID, primary key) +- email (string, unique) +- password_hash (string) +- name (string) +- age (integer) +- sex (enum: male, female, other) +- height_cm (decimal) +- current_weight_kg (decimal) +- target_weight_kg (decimal) +- activity_level (enum: sedentary, light, moderate, active, very_active) +- steps_goal (integer, default: 10000) +- goal_type (enum: weight_loss, detox, fat_burn, maintenance, muscle_gain) +- created_at (timestamp) +- updated_at (timestamp) +``` + +### 2. Tabla: `meals` +```sql +- id (UUID, primary key) +- user_id (UUID, foreign key) +- date (date) +- meal_type (enum: breakfast, lunch, snack, dinner) +- name (string) +- description (text) +- photo_url (string, nullable) +- total_calories (decimal) +- protein_g (decimal) +- carbs_g (decimal) +- fat_g (decimal) +- fiber_g (decimal) +- created_at (timestamp) +``` + +### 3. Tabla: `food_items` +```sql +- id (UUID, primary key) +- meal_id (UUID, foreign key) +- food_name (string) +- quantity (decimal) +- unit (string: g, ml, oz, cup, etc.) +- calories (decimal) +- protein_g (decimal) +- carbs_g (decimal) +- fat_g (decimal) +``` + +### 4. Tabla: `recipes` +```sql +- id (UUID, primary key) +- name (string) +- description (text) +- servings (integer) +- prep_time_minutes (integer) +- cook_time_minutes (integer) +- instructions (json) +- ingredients (json) +- calories_per_serving (decimal) +- protein_g (decimal) +- carbs_g (decimal) +- fat_g (decimal) +- tags (array: low-carb, vegan, gluten-free, etc.) +``` + +### 5. Tabla: `weekly_plans` +```sql +- id (UUID, primary key) +- user_id (UUID, foreign key) +- week_start_date (date) +- goal_calories_per_day (decimal) +- meals_plan (json) # Estructura de 7 días con comidas +- shopping_list (json) +- created_at (timestamp) +``` + +### 6. Tabla: `weight_logs` +```sql +- id (UUID, primary key) +- user_id (UUID, foreign key) +- weight_kg (decimal) +- date (date) +- notes (text, nullable) +``` + +--- + +## 🤖 Sistema de Análisis de Fotos + +### Flujo de Procesamiento + +``` +1. Usuario sube foto + ↓ +2. Validación de imagen (formato, tamaño) + ↓ +3. Envío a GPT-4 Vision API + ↓ +4. Prompt: "Identifica todos los alimentos en esta imagen, + estima las porciones y proporciona información + nutricional en formato JSON" + ↓ +5. Respuesta JSON estructurada + { + "foods": [ + { + "name": "arroz blanco", + "quantity": 150, + "unit": "g", + "calories": 195, + "protein_g": 4, + "carbs_g": 43, + "fat_g": 0.4 + }, + ... + ] + } + ↓ +6. Almacenamiento en BD + cálculo de totales +``` + +### Alternativa: Modelo Local +- Usar **CLIP** (Contrastive Language-Image Pre-training) +- Fine-tuning en dataset de comidas (Food-101, Nutrition5k) +- Más barato pero menos preciso + +--- + +## 🧮 Calculadora de Necesidades Calóricas + +### Fórmulas Implementadas + +#### 1. TMB (Tasa Metabólica Basal) - Mifflin-St Jeor +```python +# Hombres +TMB = (10 × peso_kg) + (6.25 × altura_cm) - (5 × edad) + 5 + +# Mujeres +TMB = (10 × peso_kg) + (6.25 × altura_cm) - (5 × edad) - 161 +``` + +#### 2. TDEE (Gasto Energético Total Diario) +```python +multiplicadores = { + 'sedentary': 1.2, # Poco o ningún ejercicio + 'light': 1.375, # Ejercicio ligero 1-3 días/semana + 'moderate': 1.55, # Ejercicio moderado 3-5 días/semana + 'active': 1.725, # Ejercicio intenso 6-7 días/semana + 'very_active': 1.9 # Ejercicio muy intenso + trabajo físico +} + +TDEE = TMB × multiplicadores[activity_level] +``` + +#### 3. Calorías Objetivo según Meta +```python +objetivos = { + 'weight_loss': TDEE - 500, # Déficit de 500 cal (0.5 kg/semana) + 'fat_burn': TDEE - 300, # Déficit moderado + 'detox': TDEE, # Mantenimiento con alimentos limpios + 'maintenance': TDEE, + 'muscle_gain': TDEE + 300 # Superávit de 300 cal +} +``` + +#### 4. Distribución de Macronutrientes +```python +# Pérdida de peso / Quema de grasa +protein_pct = 0.30 # 30% proteína +carbs_pct = 0.35 # 35% carbohidratos +fat_pct = 0.35 # 35% grasas + +# Detox +protein_pct = 0.25 +carbs_pct = 0.45 # Más carbohidratos de frutas/verduras +fat_pct = 0.30 + +# Ganancia muscular +protein_pct = 0.35 # Más proteína +carbs_pct = 0.45 +fat_pct = 0.20 +``` + +--- + +## 🍽️ Motor de Recomendaciones de Comidas + +### Algoritmo de Generación de Planes + +```python +def generar_plan_semanal(user_profile, preferences): + """ + 1. Calcular calorías objetivo diarias + 2. Distribuir calorías por comida: + - Desayuno: 25% + - Almuerzo: 35% + - Merienda: 10% + - Cena: 30% + + 3. Filtrar recetas según: + - Calorías por porción + - Macros compatibles + - Restricciones (vegano, sin gluten, etc.) + - Tiempo de preparación + + 4. Optimizar variedad: + - No repetir proteína principal > 2 días seguidos + - Alternar tipos de cocina + - Balancear colores (frutas/verduras) + + 5. Generar lista de compras consolidada + """ +``` + +### Criterios de Optimización +- **Balance nutricional**: Cumplir macros objetivo ±5% +- **Variedad**: Máximo 2 repeticiones de ingrediente principal/semana +- **Estacionalidad**: Priorizar alimentos de temporada +- **Costo**: Dentro del presupuesto definido +- **Tiempo**: No exceder tiempo de preparación disponible + +--- + +## 🎨 Diseño de Interfaz de Usuario + +### Pantallas Principales + +#### 1. **Onboarding** +- Bienvenida +- Registro de datos personales +- Selección de objetivo +- Configuración de preferencias + +#### 2. **Dashboard** +``` +┌─────────────────────────────────────┐ +│ Hoy: 1,450 / 1,800 cal │ +│ ████████░░░░ 80% │ +│ │ +│ Macros: │ +│ Proteína: 68g / 135g ████░░░ │ +│ Carbos: 150g / 202g ███████░ │ +│ Grasas: 45g / 60g ███████░ │ +│ │ +│ [+ Agregar Comida] │ +└─────────────────────────────────────┘ +``` + +#### 3. **Registro de Comida** +- Tabs: Foto | Descripción | Manual +- Vista previa de análisis nutricional +- Edición de cantidades +- Guardar comida + +#### 4. **Plan Semanal** +- Calendario con comidas asignadas +- Vista de receta al hacer click +- Botón "Generar nuevo plan" +- Lista de compras descargable + +#### 5. **Progreso** +- Gráficos de peso (línea temporal) +- Calorías diarias (gráfico de barras) +- Macros promedio (gráfico de dona) +- Streak y logros + +--- + +## 🔐 Seguridad y Privacidad + +### Medidas Implementadas +- **Encriptación**: Passwords con bcrypt (12 rounds) +- **Autenticación**: JWT con refresh tokens +- **HTTPS**: Obligatorio en producción +- **Rate limiting**: Prevenir abuso de APIs +- **Validación**: Sanitización de inputs +- **GDPR**: Opción de exportar/eliminar datos + +--- + +## 📦 APIs y Bases de Datos de Alimentos + +### Opción 1: USDA FoodData Central (GRATIS) +``` +- Base de datos oficial del gobierno USA +- 390,000+ alimentos +- Información nutricional completa +- API gratuita sin límite de requests +- URL: https://fdc.nal.usda.gov/api-guide.html +``` + +### Opción 2: Nutritionix API +``` +- Base de datos comercial +- 800,000+ alimentos +- Incluye alimentos de restaurantes +- 500 requests/día gratis +- $79/mes para plan pro +``` + +### Opción 3: Spoonacular API +``` +- Enfocado en recetas +- 5,000+ recetas con análisis nutricional +- Generador de planes de comidas +- 150 requests/día gratis +- $49/mes para plan básico +``` + +### Recomendación +Usar **USDA FoodData Central** como base principal + **Spoonacular** para recetas y generación de planes. + +--- + +## 🚀 Plan de Implementación por Fases + +### Fase 1: MVP (4 semanas) +- ✅ Backend básico (FastAPI + PostgreSQL) +- ✅ Autenticación de usuarios +- ✅ Perfil de usuario con cálculo de calorías +- ✅ Registro manual de comidas +- ✅ Dashboard con calorías y macros del día +- ✅ Integración con USDA API + +### Fase 2: IA de Fotos (2 semanas) +- ✅ Integración con GPT-4 Vision +- ✅ Upload de fotos +- ✅ Análisis automático de alimentos +- ✅ Validación y edición de resultados + +### Fase 3: Parser de Recetas (2 semanas) +- ✅ NLP para procesar descripciones +- ✅ Extracción de ingredientes y cantidades +- ✅ Cálculo nutricional de recetas + +### Fase 4: Planes Semanales (3 semanas) +- ✅ Motor de recomendaciones +- ✅ Integración con Spoonacular +- ✅ Generador de listas de compras +- ✅ Algoritmo de optimización + +### Fase 5: Frontend Web (3 semanas) +- ✅ React + TypeScript +- ✅ Todas las pantallas principales +- ✅ Gráficos de progreso +- ✅ Responsive design + +### Fase 6: App Móvil (4 semanas) +- ✅ React Native +- ✅ Cámara integrada +- ✅ Notificaciones push +- ✅ Modo offline + +--- + +## 💰 Estimación de Costos Mensuales + +### Desarrollo +- Gratis (open source / proyecto personal) + +### Infraestructura (1,000 usuarios activos) +``` +- Backend (Railway): $5-15/mes +- Base de datos (PostgreSQL): $5/mes (Railway) +- Redis (Upstash): Gratis hasta 10K requests/día +- Storage de fotos (Cloudflare R2): $0.015/GB +- Total: ~$20/mes +``` + +### APIs Externas +``` +- USDA FoodData: Gratis +- GPT-4 Vision (OpenAI): + - $0.01 por imagen (alta calidad) + - 100 fotos/día = $30/mes +- Spoonacular API: $49/mes (plan básico) +- Total: ~$80/mes +``` + +**Costo total estimado: ~$100/mes** para MVP con 100 usuarios activos. + +--- + +## 📈 Métricas de Éxito + +### KPIs Principales +- **Retención**: % usuarios activos después de 30 días (objetivo: >40%) +- **Engagement**: Promedio de comidas registradas por semana (objetivo: >15) +- **Precisión IA**: Exactitud de análisis de fotos (objetivo: >85%) +- **Satisfacción**: NPS (Net Promoter Score) (objetivo: >50) + +### Métricas Técnicas +- **Uptime**: 99.5% disponibilidad +- **Tiempo de respuesta API**: <200ms (p95) +- **Tiempo de análisis de foto**: <5 segundos + +--- + +## 🔮 Roadmap Futuro + +### Features Adicionales +- 🤝 **Comunidad**: Compartir recetas, progreso, desafíos +- 🏋️ **Integración con wearables**: Fitbit, Apple Watch, Google Fit +- 💊 **Tracking de suplementos**: Vitaminas, proteína en polvo +- 🧘 **Bienestar integral**: Sueño, estrés, meditación +- 🎯 **Gamificación**: Puntos, logros, desafíos semanales +- 👨‍⚕️ **Panel para nutricionistas**: Seguimiento de clientes +- 🌍 **Multi-idioma**: Español, inglés, portugués +- 🛒 **Integración con supermercados**: Pedidos online desde la app + +--- + +## 📚 Recursos y Referencias + +### Documentación Técnica +- FastAPI: https://fastapi.tiangolo.com/ +- React: https://react.dev/ +- GPT-4 Vision: https://platform.openai.com/docs/guides/vision +- USDA API: https://fdc.nal.usda.gov/api-guide.html + +### Investigación Nutricional +- Mifflin-St Jeor Equation: https://pubmed.ncbi.nlm.nih.gov/2305711/ +- Macronutrient Distribution: AMDR (Acceptable Macronutrient Distribution Ranges) +- TDEE Calculators: https://tdeecalculator.net/ + +### Datasets de Entrenamiento +- Food-101: https://data.vision.ee.ethz.ch/cvl/datasets_extra/food-101/ +- Nutrition5k: https://github.com/google-research-datasets/Nutrition5k + +--- + +## ✅ Próximos Pasos + +1. **Validar requisitos** con stakeholders/usuarios potenciales +2. **Definir scope del MVP** (empezar con Fase 1) +3. **Configurar entorno de desarrollo** +4. **Crear repositorio Git** con estructura de proyecto +5. **Setup de base de datos** (PostgreSQL local) +6. **Implementar autenticación** y endpoints básicos +7. **Probar integración** con USDA API +8. **Desarrollar frontend** básico para testing + +--- + +**Documento creado**: 2026-01-02 +**Versión**: 1.0 +**Estado**: Borrador para aprobación diff --git a/NUTRITION_APP_STRUCTURE.md b/NUTRITION_APP_STRUCTURE.md new file mode 100644 index 0000000..aea1c94 --- /dev/null +++ b/NUTRITION_APP_STRUCTURE.md @@ -0,0 +1,542 @@ +# 📁 Estructura del Proyecto - Aplicación de Nutrición + +## Arquitectura de Carpetas + +``` +nutrition-tracker/ +│ +├── 📂 backend/ # API Backend (FastAPI) +│ ├── 📂 app/ +│ │ ├── 📂 api/ # Endpoints de la API +│ │ │ ├── v1/ +│ │ │ │ ├── __init__.py +│ │ │ │ ├── auth.py # Login, registro, JWT +│ │ │ │ ├── users.py # Gestión de usuarios +│ │ │ │ ├── meals.py # CRUD de comidas +│ │ │ │ ├── photos.py # Análisis de fotos +│ │ │ │ ├── recipes.py # Parser de recetas +│ │ │ │ ├── plans.py # Planes semanales +│ │ │ │ ├── nutrition.py # Cálculos nutricionales +│ │ │ │ └── progress.py # Tracking de progreso +│ │ │ └── deps.py # Dependencies (auth, db) +│ │ │ +│ │ ├── 📂 core/ # Configuración core +│ │ │ ├── __init__.py +│ │ │ ├── config.py # Settings (env vars) +│ │ │ ├── security.py # JWT, hashing +│ │ │ └── logging.py # Configuración de logs +│ │ │ +│ │ ├── 📂 db/ # Base de datos +│ │ │ ├── __init__.py +│ │ │ ├── base.py # Base class para modelos +│ │ │ ├── session.py # Database session +│ │ │ └── init_db.py # Inicialización +│ │ │ +│ │ ├── 📂 models/ # Modelos SQLAlchemy +│ │ │ ├── __init__.py +│ │ │ ├── user.py # Modelo User +│ │ │ ├── meal.py # Modelo Meal +│ │ │ ├── food_item.py # Modelo FoodItem +│ │ │ ├── recipe.py # Modelo Recipe +│ │ │ ├── weekly_plan.py # Modelo WeeklyPlan +│ │ │ └── weight_log.py # Modelo WeightLog +│ │ │ +│ │ ├── 📂 schemas/ # Pydantic schemas (validación) +│ │ │ ├── __init__.py +│ │ │ ├── user.py # UserCreate, UserResponse +│ │ │ ├── meal.py # MealCreate, MealResponse +│ │ │ ├── recipe.py # RecipeCreate, RecipeResponse +│ │ │ ├── plan.py # PlanCreate, PlanResponse +│ │ │ └── token.py # Token, TokenPayload +│ │ │ +│ │ ├── 📂 services/ # Lógica de negocio +│ │ │ ├── __init__.py +│ │ │ ├── ai_vision.py # Integración GPT-4 Vision +│ │ │ ├── nutrition_calc.py # Cálculo TMB, TDEE, macros +│ │ │ ├── usda_api.py # Integración USDA FoodData +│ │ │ ├── spoonacular_api.py # Integración Spoonacular +│ │ │ ├── recipe_parser.py # NLP para recetas +│ │ │ ├── plan_generator.py # Generador de planes semanales +│ │ │ └── storage.py # Upload de fotos (Cloudflare R2) +│ │ │ +│ │ ├── 📂 utils/ # Utilidades +│ │ │ ├── __init__.py +│ │ │ ├── validators.py # Validadores custom +│ │ │ ├── constants.py # Constantes (macros, etc.) +│ │ │ └── helpers.py # Funciones auxiliares +│ │ │ +│ │ ├── main.py # Punto de entrada de FastAPI +│ │ └── __init__.py +│ │ +│ ├── 📂 alembic/ # Migraciones de BD +│ │ ├── versions/ +│ │ ├── env.py +│ │ └── alembic.ini +│ │ +│ ├── 📂 tests/ # Tests +│ │ ├── __init__.py +│ │ ├── conftest.py # Fixtures pytest +│ │ ├── test_auth.py +│ │ ├── test_meals.py +│ │ ├── test_nutrition_calc.py +│ │ └── test_ai_vision.py +│ │ +│ ├── .env.example # Template de variables +│ ├── .gitignore +│ ├── requirements.txt # Dependencias Python +│ ├── pytest.ini +│ └── README.md +│ +├── 📂 frontend/ # Frontend Web (React) +│ ├── 📂 public/ +│ │ ├── index.html +│ │ ├── favicon.ico +│ │ └── manifest.json +│ │ +│ ├── 📂 src/ +│ │ ├── 📂 components/ # Componentes React +│ │ │ ├── 📂 auth/ +│ │ │ │ ├── LoginForm.tsx +│ │ │ │ ├── RegisterForm.tsx +│ │ │ │ └── ProtectedRoute.tsx +│ │ │ │ +│ │ │ ├── 📂 dashboard/ +│ │ │ │ ├── CaloriesProgress.tsx +│ │ │ │ ├── MacrosChart.tsx +│ │ │ │ ├── DailyMeals.tsx +│ │ │ │ └── QuickStats.tsx +│ │ │ │ +│ │ │ ├── 📂 meals/ +│ │ │ │ ├── MealForm.tsx +│ │ │ │ ├── PhotoUpload.tsx +│ │ │ │ ├── RecipeInput.tsx +│ │ │ │ ├── ManualEntry.tsx +│ │ │ │ └── MealCard.tsx +│ │ │ │ +│ │ │ ├── 📂 plans/ +│ │ │ │ ├── WeeklyCalendar.tsx +│ │ │ │ ├── RecipeDetail.tsx +│ │ │ │ ├── ShoppingList.tsx +│ │ │ │ └── PlanGenerator.tsx +│ │ │ │ +│ │ │ ├── 📂 profile/ +│ │ │ │ ├── UserProfile.tsx +│ │ │ │ ├── GoalSettings.tsx +│ │ │ │ └── PreferencesForm.tsx +│ │ │ │ +│ │ │ ├── 📂 progress/ +│ │ │ │ ├── WeightChart.tsx +│ │ │ │ ├── CaloriesChart.tsx +│ │ │ │ ├── StreakCounter.tsx +│ │ │ │ └── AchievementsList.tsx +│ │ │ │ +│ │ │ └── 📂 common/ +│ │ │ ├── Button.tsx +│ │ │ ├── Card.tsx +│ │ │ ├── Input.tsx +│ │ │ ├── Modal.tsx +│ │ │ ├── Loading.tsx +│ │ │ └── ErrorBoundary.tsx +│ │ │ +│ │ ├── 📂 hooks/ # Custom hooks +│ │ │ ├── useAuth.ts +│ │ │ ├── useMeals.ts +│ │ │ ├── usePlans.ts +│ │ │ └── useNutrition.ts +│ │ │ +│ │ ├── 📂 services/ # Servicios API +│ │ │ ├── api.ts # Axios config +│ │ │ ├── authService.ts +│ │ │ ├── mealService.ts +│ │ │ ├── planService.ts +│ │ │ └── userService.ts +│ │ │ +│ │ ├── 📂 store/ # Estado global (Zustand) +│ │ │ ├── authStore.ts +│ │ │ ├── mealStore.ts +│ │ │ └── userStore.ts +│ │ │ +│ │ ├── 📂 types/ # TypeScript types +│ │ │ ├── user.ts +│ │ │ ├── meal.ts +│ │ │ ├── recipe.ts +│ │ │ └── plan.ts +│ │ │ +│ │ ├── 📂 utils/ # Utilidades +│ │ │ ├── formatters.ts # Format dates, numbers +│ │ │ ├── validators.ts +│ │ │ └── constants.ts +│ │ │ +│ │ ├── 📂 pages/ # Páginas principales +│ │ │ ├── Login.tsx +│ │ │ ├── Register.tsx +│ │ │ ├── Onboarding.tsx +│ │ │ ├── Dashboard.tsx +│ │ │ ├── AddMeal.tsx +│ │ │ ├── WeeklyPlan.tsx +│ │ │ ├── Progress.tsx +│ │ │ └── Settings.tsx +│ │ │ +│ │ ├── App.tsx # Componente principal +│ │ ├── index.tsx # Entry point +│ │ └── index.css # Estilos globales +│ │ +│ ├── .env.example +│ ├── .gitignore +│ ├── package.json +│ ├── tsconfig.json +│ ├── tailwind.config.js +│ └── README.md +│ +├── 📂 mobile/ # App Móvil (React Native) +│ ├── 📂 src/ +│ │ ├── 📂 screens/ # Pantallas +│ │ │ ├── LoginScreen.tsx +│ │ │ ├── DashboardScreen.tsx +│ │ │ ├── CameraScreen.tsx +│ │ │ ├── AddMealScreen.tsx +│ │ │ └── ProgressScreen.tsx +│ │ │ +│ │ ├── 📂 components/ +│ │ ├── 📂 navigation/ +│ │ │ └── AppNavigator.tsx +│ │ ├── 📂 services/ +│ │ └── 📂 utils/ +│ │ +│ ├── App.tsx +│ ├── app.json +│ ├── package.json +│ └── tsconfig.json +│ +├── 📂 shared/ # Código compartido +│ ├── 📂 types/ # Types compartidos +│ └── 📂 constants/ # Constantes compartidas +│ +├── 📂 docs/ # Documentación +│ ├── API.md # Documentación de API +│ ├── DEPLOYMENT.md # Guía de deployment +│ ├── CONTRIBUTING.md # Guía de contribución +│ └── USER_GUIDE.md # Guía de usuario +│ +├── 📂 scripts/ # Scripts de utilidad +│ ├── seed_database.py # Poblar BD con datos iniciales +│ ├── import_usda_data.py # Importar datos USDA +│ └── backup_db.sh # Backup de BD +│ +├── .gitignore +├── docker-compose.yml # Docker para desarrollo +├── Dockerfile # Dockerfile para backend +├── LICENSE +└── README.md # README principal +``` + +--- + +## 🗄️ Esquema de Base de Datos (PostgreSQL) + +```sql +-- TABLA: users +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + name VARCHAR(255) NOT NULL, + age INTEGER, + sex VARCHAR(10) CHECK (sex IN ('male', 'female', 'other')), + height_cm DECIMAL(5,2), + current_weight_kg DECIMAL(5,2), + target_weight_kg DECIMAL(5,2), + activity_level VARCHAR(20) CHECK (activity_level IN ( + 'sedentary', 'light', 'moderate', 'active', 'very_active' + )), + steps_goal INTEGER DEFAULT 10000, + goal_type VARCHAR(20) CHECK (goal_type IN ( + 'weight_loss', 'detox', 'fat_burn', 'maintenance', 'muscle_gain' + )), + preferences JSONB DEFAULT '{}', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- TABLA: meals +CREATE TABLE meals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + date DATE NOT NULL, + meal_type VARCHAR(20) CHECK (meal_type IN ( + 'breakfast', 'lunch', 'snack', 'dinner' + )), + name VARCHAR(255), + description TEXT, + photo_url VARCHAR(500), + total_calories DECIMAL(7,2), + protein_g DECIMAL(6,2), + carbs_g DECIMAL(6,2), + fat_g DECIMAL(6,2), + fiber_g DECIMAL(5,2), + sugar_g DECIMAL(5,2), + sodium_mg DECIMAL(6,2), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_date (user_id, date) +); + +-- TABLA: food_items +CREATE TABLE food_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + meal_id UUID REFERENCES meals(id) ON DELETE CASCADE, + food_name VARCHAR(255) NOT NULL, + quantity DECIMAL(8,2), + unit VARCHAR(20), + calories DECIMAL(7,2), + protein_g DECIMAL(6,2), + carbs_g DECIMAL(6,2), + fat_g DECIMAL(6,2), + fiber_g DECIMAL(5,2), + usda_fdc_id INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- TABLA: recipes +CREATE TABLE recipes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + description TEXT, + servings INTEGER, + prep_time_minutes INTEGER, + cook_time_minutes INTEGER, + instructions JSONB, + ingredients JSONB, + calories_per_serving DECIMAL(7,2), + protein_g DECIMAL(6,2), + carbs_g DECIMAL(6,2), + fat_g DECIMAL(6,2), + tags TEXT[], + cuisine_type VARCHAR(50), + difficulty VARCHAR(20), + image_url VARCHAR(500), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- TABLA: weekly_plans +CREATE TABLE weekly_plans ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + week_start_date DATE NOT NULL, + goal_calories_per_day DECIMAL(7,2), + meals_plan JSONB NOT NULL, + shopping_list JSONB, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_week (user_id, week_start_date) +); + +-- TABLA: weight_logs +CREATE TABLE weight_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + weight_kg DECIMAL(5,2) NOT NULL, + date DATE NOT NULL, + notes TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_date (user_id, date) +); + +-- TABLA: user_achievements (opcional - gamificación) +CREATE TABLE user_achievements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE CASCADE, + achievement_type VARCHAR(50), + earned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + metadata JSONB +); +``` + +--- + +## 🔧 Configuración de Desarrollo + +### Backend (.env) +```env +# Database +DATABASE_URL=postgresql://user:password@localhost:5432/nutrition_db +REDIS_URL=redis://localhost:6379/0 + +# Security +SECRET_KEY=your-secret-key-here +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# External APIs +OPENAI_API_KEY=sk-... +USDA_API_KEY=your-usda-key +SPOONACULAR_API_KEY=your-spoonacular-key + +# Storage +CLOUDFLARE_R2_ACCESS_KEY=... +CLOUDFLARE_R2_SECRET_KEY=... +CLOUDFLARE_R2_BUCKET=nutrition-photos + +# Environment +ENVIRONMENT=development +DEBUG=True +CORS_ORIGINS=http://localhost:3000,http://localhost:19006 +``` + +### Frontend (.env) +```env +REACT_APP_API_URL=http://localhost:8000/api/v1 +REACT_APP_ENVIRONMENT=development +``` + +--- + +## 🚀 Comandos de Desarrollo + +### Backend +```bash +# Crear entorno virtual +python -m venv venv +source venv/bin/activate # Linux/Mac +venv\Scripts\activate # Windows + +# Instalar dependencias +pip install -r requirements.txt + +# Crear base de datos +createdb nutrition_db + +# Ejecutar migraciones +alembic upgrade head + +# Poblar base de datos con datos de ejemplo +python scripts/seed_database.py + +# Ejecutar servidor de desarrollo +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# Ejecutar tests +pytest + +# Generar migración +alembic revision --autogenerate -m "descripcion" +``` + +### Frontend +```bash +# Instalar dependencias +npm install + +# Ejecutar en desarrollo +npm start + +# Build para producción +npm run build + +# Ejecutar tests +npm test +``` + +### Docker (Desarrollo completo) +```bash +# Levantar todos los servicios +docker-compose up -d + +# Ver logs +docker-compose logs -f + +# Detener servicios +docker-compose down +``` + +--- + +## 📦 Dependencias Principales + +### Backend (requirements.txt) +```txt +# Framework +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Database +sqlalchemy==2.0.25 +alembic==1.13.1 +psycopg2-binary==2.9.9 +asyncpg==0.29.0 + +# Authentication +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 + +# External APIs +openai==1.12.0 +httpx==0.26.0 +aiohttp==3.9.1 + +# Utilities +python-dotenv==1.0.0 +pillow==10.2.0 +redis==5.0.1 + +# Testing +pytest==7.4.4 +pytest-asyncio==0.23.3 +pytest-cov==4.1.0 +``` + +### Frontend (package.json) +```json +{ + "dependencies": { + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.21.0", + "typescript": "^5.3.3", + "axios": "^1.6.5", + "zustand": "^4.4.7", + "@tanstack/react-query": "^5.17.9", + "recharts": "^2.10.3", + "date-fns": "^3.2.0", + "react-hook-form": "^7.49.3", + "zod": "^3.22.4", + "tailwindcss": "^3.4.1", + "@headlessui/react": "^1.7.18", + "lucide-react": "^0.309.0" + } +} +``` + +--- + +## 🎯 MVP - Archivos Prioritarios a Implementar + +### Semana 1-2: Backend Core +1. `backend/app/main.py` - Entry point FastAPI +2. `backend/app/core/config.py` - Configuración +3. `backend/app/core/security.py` - Auth JWT +4. `backend/app/db/session.py` - Database session +5. `backend/app/models/user.py` - Modelo User +6. `backend/app/schemas/user.py` - Schemas User +7. `backend/app/api/v1/auth.py` - Login/Register endpoints +8. `backend/app/services/nutrition_calc.py` - Cálculo TMB/TDEE + +### Semana 3-4: Features Core +9. `backend/app/models/meal.py` - Modelo Meal +10. `backend/app/api/v1/meals.py` - CRUD meals +11. `backend/app/services/usda_api.py` - Integración USDA +12. `backend/app/api/v1/nutrition.py` - Dashboard nutrition + +### Semana 5-6: Frontend Básico +13. `frontend/src/services/api.ts` - Axios config +14. `frontend/src/pages/Login.tsx` +15. `frontend/src/pages/Dashboard.tsx` +16. `frontend/src/components/dashboard/CaloriesProgress.tsx` +17. `frontend/src/pages/AddMeal.tsx` + +--- + +**Documento creado**: 2026-01-02 +**Versión**: 1.0 diff --git a/NUTRITION_CODE_EXAMPLES.md b/NUTRITION_CODE_EXAMPLES.md new file mode 100644 index 0000000..e0b51e4 --- /dev/null +++ b/NUTRITION_CODE_EXAMPLES.md @@ -0,0 +1,1143 @@ +# 💻 Ejemplos de Código - Aplicación de Nutrición + +## 📝 Índice +1. [Backend - Cálculo Nutricional](#backend-calculo-nutricional) +2. [Backend - Análisis de Fotos con IA](#backend-analisis-fotos) +3. [Backend - Integración USDA API](#backend-usda-api) +4. [Backend - Generador de Planes](#backend-generador-planes) +5. [Backend - Autenticación JWT](#backend-auth-jwt) +6. [Frontend - Dashboard de Calorías](#frontend-dashboard) +7. [Frontend - Upload de Fotos](#frontend-upload-fotos) +8. [Modelos de Base de Datos](#modelos-database) + +--- + +## 1️⃣ Backend - Cálculo Nutricional + +### `backend/app/services/nutrition_calc.py` + +```python +""" +Servicio de cálculos nutricionales. +Incluye TMB, TDEE, macros y calorías objetivo. +""" +from enum import Enum +from typing import Dict, Tuple +from pydantic import BaseModel + + +class Sex(str, Enum): + MALE = "male" + FEMALE = "female" + OTHER = "other" + + +class ActivityLevel(str, Enum): + SEDENTARY = "sedentary" # 1.2 + LIGHT = "light" # 1.375 + MODERATE = "moderate" # 1.55 + ACTIVE = "active" # 1.725 + VERY_ACTIVE = "very_active" # 1.9 + + +class GoalType(str, Enum): + WEIGHT_LOSS = "weight_loss" # -500 cal + FAT_BURN = "fat_burn" # -300 cal + DETOX = "detox" # maintenance + MAINTENANCE = "maintenance" # +0 cal + MUSCLE_GAIN = "muscle_gain" # +300 cal + + +class MacroDistribution(BaseModel): + """Distribución de macronutrientes en gramos""" + protein_g: float + carbs_g: float + fat_g: float + calories: float + + +class NutritionCalculator: + """Calculadora de necesidades nutricionales""" + + # Multiplicadores de actividad para TDEE + ACTIVITY_MULTIPLIERS = { + ActivityLevel.SEDENTARY: 1.2, + ActivityLevel.LIGHT: 1.375, + ActivityLevel.MODERATE: 1.55, + ActivityLevel.ACTIVE: 1.725, + ActivityLevel.VERY_ACTIVE: 1.9, + } + + # Ajuste calórico según objetivo + GOAL_ADJUSTMENTS = { + GoalType.WEIGHT_LOSS: -500, + GoalType.FAT_BURN: -300, + GoalType.DETOX: 0, + GoalType.MAINTENANCE: 0, + GoalType.MUSCLE_GAIN: 300, + } + + # Distribución de macros según objetivo (proteína, carbos, grasas) + MACRO_DISTRIBUTIONS = { + GoalType.WEIGHT_LOSS: (0.30, 0.35, 0.35), + GoalType.FAT_BURN: (0.30, 0.35, 0.35), + GoalType.DETOX: (0.25, 0.45, 0.30), + GoalType.MAINTENANCE: (0.25, 0.45, 0.30), + GoalType.MUSCLE_GAIN: (0.35, 0.45, 0.20), + } + + @staticmethod + def calculate_bmr( + weight_kg: float, + height_cm: float, + age: int, + sex: Sex + ) -> float: + """ + Calcular TMB (Tasa Metabólica Basal) usando Mifflin-St Jeor. + + Fórmula: + - Hombres: (10 × peso) + (6.25 × altura) - (5 × edad) + 5 + - Mujeres: (10 × peso) + (6.25 × altura) - (5 × edad) - 161 + + Args: + weight_kg: Peso en kilogramos + height_cm: Altura en centímetros + age: Edad en años + sex: Sexo del usuario + + Returns: + TMB en calorías/día + """ + bmr = (10 * weight_kg) + (6.25 * height_cm) - (5 * age) + + if sex == Sex.MALE: + bmr += 5 + elif sex == Sex.FEMALE: + bmr -= 161 + else: + # Para 'other', usar promedio + bmr -= 78 + + return round(bmr, 2) + + @staticmethod + def calculate_tdee(bmr: float, activity_level: ActivityLevel) -> float: + """ + Calcular TDEE (Gasto Energético Total Diario). + + TDEE = TMB × multiplicador de actividad + + Args: + bmr: Tasa Metabólica Basal + activity_level: Nivel de actividad del usuario + + Returns: + TDEE en calorías/día + """ + multiplier = NutritionCalculator.ACTIVITY_MULTIPLIERS[activity_level] + tdee = bmr * multiplier + return round(tdee, 2) + + @staticmethod + def calculate_target_calories( + tdee: float, + goal_type: GoalType + ) -> float: + """ + Calcular calorías objetivo según meta. + + Args: + tdee: Gasto energético total diario + goal_type: Tipo de objetivo del usuario + + Returns: + Calorías objetivo diarias + """ + adjustment = NutritionCalculator.GOAL_ADJUSTMENTS[goal_type] + target = tdee + adjustment + + # Asegurar mínimo saludable (1200 cal para mujeres, 1500 para hombres) + min_calories = 1200 + return max(round(target, 2), min_calories) + + @staticmethod + def calculate_macros( + target_calories: float, + goal_type: GoalType + ) -> MacroDistribution: + """ + Calcular distribución de macronutrientes. + + Calorías por gramo: + - Proteína: 4 cal/g + - Carbohidratos: 4 cal/g + - Grasas: 9 cal/g + + Args: + target_calories: Calorías objetivo diarias + goal_type: Tipo de objetivo + + Returns: + Distribución de macros en gramos + """ + protein_pct, carbs_pct, fat_pct = ( + NutritionCalculator.MACRO_DISTRIBUTIONS[goal_type] + ) + + # Calcular gramos de cada macro + protein_g = round((target_calories * protein_pct) / 4, 1) + carbs_g = round((target_calories * carbs_pct) / 4, 1) + fat_g = round((target_calories * fat_pct) / 9, 1) + + return MacroDistribution( + protein_g=protein_g, + carbs_g=carbs_g, + fat_g=fat_g, + calories=target_calories + ) + + @classmethod + def calculate_full_profile( + cls, + weight_kg: float, + height_cm: float, + age: int, + sex: Sex, + activity_level: ActivityLevel, + goal_type: GoalType + ) -> Dict[str, any]: + """ + Calcular perfil nutricional completo. + + Returns: + Diccionario con BMR, TDEE, calorías objetivo y macros + """ + bmr = cls.calculate_bmr(weight_kg, height_cm, age, sex) + tdee = cls.calculate_tdee(bmr, activity_level) + target_calories = cls.calculate_target_calories(tdee, goal_type) + macros = cls.calculate_macros(target_calories, goal_type) + + return { + "bmr": bmr, + "tdee": tdee, + "target_calories": target_calories, + "macros": macros.dict(), + "activity_level": activity_level.value, + "goal_type": goal_type.value + } + + +# Ejemplo de uso +if __name__ == "__main__": + calc = NutritionCalculator() + + # Usuario ejemplo: Mujer, 30 años, 65kg, 165cm, moderadamente activa + profile = calc.calculate_full_profile( + weight_kg=65, + height_cm=165, + age=30, + sex=Sex.FEMALE, + activity_level=ActivityLevel.MODERATE, + goal_type=GoalType.WEIGHT_LOSS + ) + + print("📊 Perfil Nutricional:") + print(f" TMB: {profile['bmr']} cal/día") + print(f" TDEE: {profile['tdee']} cal/día") + print(f" Calorías objetivo: {profile['target_calories']} cal/día") + print(f" Proteína: {profile['macros']['protein_g']}g") + print(f" Carbohidratos: {profile['macros']['carbs_g']}g") + print(f" Grasas: {profile['macros']['fat_g']}g") +``` + +**Salida esperada:** +``` +📊 Perfil Nutricional: + TMB: 1363.75 cal/día + TDEE: 2113.81 cal/día + Calorías objetivo: 1613.81 cal/día + Proteína: 121.0g + Carbohidratos: 141.4g + Grasas: 62.8g +``` + +--- + +## 2️⃣ Backend - Análisis de Fotos con IA + +### `backend/app/services/ai_vision.py` + +```python +""" +Servicio de análisis de fotos de comida usando GPT-4 Vision. +""" +import base64 +from typing import List, Dict +import httpx +from pathlib import Path +from pydantic import BaseModel + + +class FoodItem(BaseModel): + """Item de comida detectado en la foto""" + name: str + quantity: float + unit: str + calories: float + protein_g: float + carbs_g: float + fat_g: float + confidence: float # 0.0 - 1.0 + + +class MealAnalysis(BaseModel): + """Resultado del análisis de una comida""" + foods: List[FoodItem] + total_calories: float + total_protein_g: float + total_carbs_g: float + total_fat_g: float + description: str + + +class AIVisionService: + """Servicio para analizar fotos de comida con GPT-4 Vision""" + + def __init__(self, api_key: str): + self.api_key = api_key + self.base_url = "https://api.openai.com/v1/chat/completions" + + def encode_image(self, image_path: str) -> str: + """Codificar imagen a base64""" + with open(image_path, "rb") as image_file: + return base64.b64encode(image_file.read()).decode('utf-8') + + async def analyze_meal_photo( + self, + image_path: str, + user_notes: str = "" + ) -> MealAnalysis: + """ + Analizar foto de comida y extraer información nutricional. + + Args: + image_path: Ruta a la imagen + user_notes: Notas adicionales del usuario sobre la comida + + Returns: + Análisis completo de la comida + """ + # Codificar imagen + base64_image = self.encode_image(image_path) + + # Crear prompt detallado + prompt = self._build_analysis_prompt(user_notes) + + # Llamar a OpenAI API + async with httpx.AsyncClient() as client: + response = await client.post( + self.base_url, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self.api_key}" + }, + json={ + "model": "gpt-4-vision-preview", + "messages": [ + { + "role": "user", + "content": [ + { + "type": "text", + "text": prompt + }, + { + "type": "image_url", + "image_url": { + "url": f"data:image/jpeg;base64,{base64_image}" + } + } + ] + } + ], + "max_tokens": 1000, + "temperature": 0.2 # Baja temperatura para más precisión + }, + timeout=30.0 + ) + + response.raise_for_status() + result = response.json() + + # Parsear respuesta JSON + content = result["choices"][0]["message"]["content"] + return self._parse_ai_response(content) + + def _build_analysis_prompt(self, user_notes: str) -> str: + """Construir prompt optimizado para análisis nutricional""" + base_prompt = """ +Analiza esta foto de comida y proporciona información nutricional detallada. + +INSTRUCCIONES: +1. Identifica TODOS los alimentos visibles en la imagen +2. Estima las porciones de cada alimento (en gramos, ml, unidades, etc.) +3. Calcula la información nutricional aproximada para cada item +4. Proporciona un nivel de confianza (0-1) para cada estimación + +FORMATO DE RESPUESTA (JSON estricto): +{ + "description": "Descripción breve de la comida", + "foods": [ + { + "name": "nombre del alimento", + "quantity": número, + "unit": "g|ml|unidad|taza|cucharada", + "calories": número, + "protein_g": número, + "carbs_g": número, + "fat_g": número, + "confidence": 0.0-1.0 + } + ] +} + +EJEMPLO: +{ + "description": "Plato de arroz con pollo y ensalada", + "foods": [ + { + "name": "pechuga de pollo a la plancha", + "quantity": 150, + "unit": "g", + "calories": 247, + "protein_g": 46.5, + "carbs_g": 0, + "fat_g": 5.4, + "confidence": 0.85 + }, + { + "name": "arroz blanco cocido", + "quantity": 200, + "unit": "g", + "calories": 260, + "protein_g": 5.3, + "carbs_g": 57, + "fat_g": 0.6, + "confidence": 0.90 + } + ] +} +""" + if user_notes: + base_prompt += f"\n\nNOTAS DEL USUARIO: {user_notes}" + + base_prompt += "\n\nRESPONDE ÚNICAMENTE CON EL JSON, SIN TEXTO ADICIONAL." + return base_prompt + + def _parse_ai_response(self, content: str) -> MealAnalysis: + """Parsear respuesta JSON de la IA""" + import json + + # Extraer JSON del contenido (por si viene con texto adicional) + start = content.find('{') + end = content.rfind('}') + 1 + json_str = content[start:end] + + data = json.loads(json_str) + + # Crear objetos FoodItem + foods = [FoodItem(**item) for item in data["foods"]] + + # Calcular totales + total_calories = sum(f.calories for f in foods) + total_protein = sum(f.protein_g for f in foods) + total_carbs = sum(f.carbs_g for f in foods) + total_fat = sum(f.fat_g for f in foods) + + return MealAnalysis( + foods=foods, + total_calories=round(total_calories, 1), + total_protein_g=round(total_protein, 1), + total_carbs_g=round(total_carbs, 1), + total_fat_g=round(total_fat, 1), + description=data.get("description", "") + ) + + +# Ejemplo de uso +async def test_vision_service(): + service = AIVisionService(api_key="sk-your-api-key") + + # Analizar foto de almuerzo + analysis = await service.analyze_meal_photo( + image_path="/path/to/lunch.jpg", + user_notes="Mi almuerzo de hoy - pollo con vegetales" + ) + + print(f"📸 {analysis.description}") + print(f"Total: {analysis.total_calories} calorías") + print(f"\nAlimentos detectados:") + for food in analysis.foods: + print(f" • {food.name}: {food.quantity}{food.unit}") + print(f" {food.calories} cal | P: {food.protein_g}g | C: {food.carbs_g}g | G: {food.fat_g}g") + print(f" Confianza: {food.confidence*100:.0f}%") +``` + +--- + +## 3️⃣ Backend - Integración USDA API + +### `backend/app/services/usda_api.py` + +```python +""" +Integración con USDA FoodData Central API. +Base de datos gratuita de 390,000+ alimentos. +""" +import httpx +from typing import List, Optional, Dict +from pydantic import BaseModel + + +class USDAFood(BaseModel): + """Alimento de la base de datos USDA""" + fdc_id: int + description: str + data_type: str + # Nutrientes por 100g + calories: Optional[float] = None + protein_g: Optional[float] = None + carbs_g: Optional[float] = None + fat_g: Optional[float] = None + fiber_g: Optional[float] = None + sugar_g: Optional[float] = None + sodium_mg: Optional[float] = None + + +class USDAAPIService: + """Cliente para USDA FoodData Central API""" + + BASE_URL = "https://api.nal.usda.gov/fdc/v1" + + def __init__(self, api_key: str): + self.api_key = api_key + + async def search_foods( + self, + query: str, + page_size: int = 10, + data_type: Optional[str] = None + ) -> List[USDAFood]: + """ + Buscar alimentos en la base de datos USDA. + + Args: + query: Término de búsqueda (ej: "banana", "chicken breast") + page_size: Número de resultados + data_type: Filtro por tipo (Survey, Foundation, Branded, etc.) + + Returns: + Lista de alimentos encontrados + """ + async with httpx.AsyncClient() as client: + params = { + "api_key": self.api_key, + "query": query, + "pageSize": page_size + } + + if data_type: + params["dataType"] = data_type + + response = await client.get( + f"{self.BASE_URL}/foods/search", + params=params, + timeout=10.0 + ) + response.raise_for_status() + + data = response.json() + foods = [] + + for item in data.get("foods", []): + food = self._parse_food_item(item) + foods.append(food) + + return foods + + async def get_food_details(self, fdc_id: int) -> USDAFood: + """ + Obtener detalles completos de un alimento por ID. + + Args: + fdc_id: Food Data Central ID + + Returns: + Alimento con información nutricional completa + """ + async with httpx.AsyncClient() as client: + response = await client.get( + f"{self.BASE_URL}/food/{fdc_id}", + params={"api_key": self.api_key}, + timeout=10.0 + ) + response.raise_for_status() + + data = response.json() + return self._parse_food_item(data) + + def _parse_food_item(self, item: Dict) -> USDAFood: + """Parsear item de alimento desde respuesta API""" + # Mapeo de IDs de nutrientes USDA + NUTRIENT_IDS = { + "calories": 1008, # Energy (kcal) + "protein": 1003, # Protein + "carbs": 1005, # Carbohydrate + "fat": 1004, # Total lipid (fat) + "fiber": 1079, # Fiber, total dietary + "sugar": 2000, # Sugars, total + "sodium": 1093 # Sodium + } + + nutrients = {} + + # Extraer nutrientes + for nutrient in item.get("foodNutrients", []): + nutrient_id = nutrient.get("nutrientId") + value = nutrient.get("value") + + if nutrient_id == NUTRIENT_IDS["calories"]: + nutrients["calories"] = value + elif nutrient_id == NUTRIENT_IDS["protein"]: + nutrients["protein_g"] = value + elif nutrient_id == NUTRIENT_IDS["carbs"]: + nutrients["carbs_g"] = value + elif nutrient_id == NUTRIENT_IDS["fat"]: + nutrients["fat_g"] = value + elif nutrient_id == NUTRIENT_IDS["fiber"]: + nutrients["fiber_g"] = value + elif nutrient_id == NUTRIENT_IDS["sugar"]: + nutrients["sugar_g"] = value + elif nutrient_id == NUTRIENT_IDS["sodium"]: + nutrients["sodium_mg"] = value + + return USDAFood( + fdc_id=item["fdcId"], + description=item["description"], + data_type=item.get("dataType", "Unknown"), + **nutrients + ) + + +# Ejemplo de uso +async def test_usda_service(): + service = USDAAPIService(api_key="your-usda-api-key") + + # Buscar "banana" + foods = await service.search_foods("banana", page_size=5) + + print("🔍 Resultados para 'banana':") + for food in foods: + print(f"\n{food.description} (ID: {food.fdc_id})") + print(f" Calorías: {food.calories or 'N/A'} kcal/100g") + print(f" Proteína: {food.protein_g or 'N/A'}g") + print(f" Carbos: {food.carbs_g or 'N/A'}g") + print(f" Grasas: {food.fat_g or 'N/A'}g") +``` + +--- + +## 4️⃣ Backend - Generador de Planes Semanales + +### `backend/app/services/plan_generator.py` + +```python +""" +Generador de planes de comidas semanales personalizados. +""" +from typing import List, Dict +from datetime import date, timedelta +from pydantic import BaseModel +from app.services.nutrition_calc import GoalType + + +class MealPlan(BaseModel): + """Plan de una comida específica""" + meal_type: str # breakfast, lunch, snack, dinner + recipe_id: str + recipe_name: str + calories: float + protein_g: float + carbs_g: float + fat_g: float + + +class DayPlan(BaseModel): + """Plan de un día completo""" + date: date + meals: List[MealPlan] + total_calories: float + total_protein_g: float + total_carbs_g: float + total_fat_g: float + + +class WeeklyPlan(BaseModel): + """Plan semanal completo""" + week_start: date + days: List[DayPlan] + shopping_list: Dict[str, Dict[str, float]] # {ingredient: {quantity, unit}} + goal_calories_per_day: float + + +class PlanGenerator: + """Generador de planes de comidas semanales""" + + # Distribución calórica por tipo de comida + MEAL_DISTRIBUTION = { + "breakfast": 0.25, # 25% + "lunch": 0.35, # 35% + "snack": 0.10, # 10% + "dinner": 0.30 # 30% + } + + def __init__(self, recipe_database): + """ + Args: + recipe_database: Instancia del servicio de recetas + """ + self.recipe_db = recipe_database + + async def generate_weekly_plan( + self, + target_calories: float, + goal_type: GoalType, + preferences: Dict, + start_date: date = None + ) -> WeeklyPlan: + """ + Generar plan semanal personalizado. + + Args: + target_calories: Calorías objetivo diarias + goal_type: Tipo de objetivo (weight_loss, detox, etc.) + preferences: Preferencias del usuario (alergias, dieta, etc.) + start_date: Fecha de inicio (por defecto: próximo lunes) + + Returns: + Plan semanal completo con recetas y lista de compras + """ + if not start_date: + start_date = self._get_next_monday() + + days = [] + all_ingredients = {} + + # Generar plan para cada día + for day_offset in range(7): + current_date = start_date + timedelta(days=day_offset) + + day_plan = await self._generate_day_plan( + current_date, + target_calories, + goal_type, + preferences + ) + + days.append(day_plan) + + # Acumular ingredientes para lista de compras + self._accumulate_ingredients(day_plan, all_ingredients) + + return WeeklyPlan( + week_start=start_date, + days=days, + shopping_list=all_ingredients, + goal_calories_per_day=target_calories + ) + + async def _generate_day_plan( + self, + date: date, + target_calories: float, + goal_type: GoalType, + preferences: Dict + ) -> DayPlan: + """Generar plan para un día específico""" + meals = [] + + # Para cada tipo de comida + for meal_type, calorie_pct in self.MEAL_DISTRIBUTION.items(): + target_meal_calories = target_calories * calorie_pct + + # Buscar receta adecuada + recipe = await self._find_suitable_recipe( + meal_type, + target_meal_calories, + goal_type, + preferences + ) + + if recipe: + meals.append(MealPlan( + meal_type=meal_type, + recipe_id=recipe["id"], + recipe_name=recipe["name"], + calories=recipe["calories"], + protein_g=recipe["protein_g"], + carbs_g=recipe["carbs_g"], + fat_g=recipe["fat_g"] + )) + + # Calcular totales del día + total_calories = sum(m.calories for m in meals) + total_protein = sum(m.protein_g for m in meals) + total_carbs = sum(m.carbs_g for m in meals) + total_fat = sum(m.fat_g for m in meals) + + return DayPlan( + date=date, + meals=meals, + total_calories=round(total_calories, 1), + total_protein_g=round(total_protein, 1), + total_carbs_g=round(total_carbs, 1), + total_fat_g=round(total_fat, 1) + ) + + async def _find_suitable_recipe( + self, + meal_type: str, + target_calories: float, + goal_type: GoalType, + preferences: Dict + ) -> Dict: + """ + Encontrar receta adecuada según criterios. + + Criterios de selección: + 1. Rango calórico: ±100 cal del objetivo + 2. Tags de objetivo (low-carb, detox, high-protein) + 3. Restricciones dietarias (vegan, gluten-free, etc.) + 4. Tiempo de preparación + """ + filters = { + "meal_type": meal_type, + "min_calories": target_calories - 100, + "max_calories": target_calories + 100, + } + + # Agregar filtros según objetivo + if goal_type in [GoalType.WEIGHT_LOSS, GoalType.FAT_BURN]: + filters["tags"] = ["high-protein", "low-carb"] + elif goal_type == GoalType.DETOX: + filters["tags"] = ["detox", "whole-foods", "anti-inflammatory"] + elif goal_type == GoalType.MUSCLE_GAIN: + filters["tags"] = ["high-protein"] + + # Aplicar preferencias del usuario + if preferences.get("diet_type"): + filters["tags"].append(preferences["diet_type"]) # vegan, vegetarian + + if preferences.get("allergies"): + filters["exclude_allergens"] = preferences["allergies"] + + if preferences.get("max_prep_time"): + filters["max_prep_time"] = preferences["max_prep_time"] + + # Buscar en la base de datos de recetas + recipes = await self.recipe_db.search(filters) + + if recipes: + # Seleccionar la mejor coincidencia + return self._select_best_recipe(recipes, target_calories) + + return None + + def _select_best_recipe( + self, + recipes: List[Dict], + target_calories: float + ) -> Dict: + """Seleccionar la receta más cercana al objetivo calórico""" + recipes_sorted = sorted( + recipes, + key=lambda r: abs(r["calories"] - target_calories) + ) + return recipes_sorted[0] + + def _accumulate_ingredients( + self, + day_plan: DayPlan, + all_ingredients: Dict + ): + """Acumular ingredientes para lista de compras""" + for meal in day_plan.meals: + # Obtener ingredientes de cada receta + recipe_ingredients = self.recipe_db.get_ingredients(meal.recipe_id) + + for ingredient in recipe_ingredients: + name = ingredient["name"] + quantity = ingredient["quantity"] + unit = ingredient["unit"] + + if name not in all_ingredients: + all_ingredients[name] = {"quantity": 0, "unit": unit} + + # Acumular cantidades + all_ingredients[name]["quantity"] += quantity + + @staticmethod + def _get_next_monday() -> date: + """Obtener fecha del próximo lunes""" + today = date.today() + days_until_monday = (7 - today.weekday()) % 7 + if days_until_monday == 0: + days_until_monday = 7 + return today + timedelta(days=days_until_monday) +``` + +--- + +## 5️⃣ Backend - Autenticación JWT + +### `backend/app/core/security.py` + +```python +""" +Seguridad: Hashing de passwords y manejo de JWT tokens. +""" +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + + +class TokenData(BaseModel): + """Datos contenidos en el token JWT""" + user_id: str + email: str + + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# Configuración (en producción, usar variables de entorno) +SECRET_KEY = "your-secret-key-here-change-in-production" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 +REFRESH_TOKEN_EXPIRE_DAYS = 7 + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verificar que el password coincida con el hash""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Generar hash bcrypt del password""" + return pwd_context.hash(password) + + +def create_access_token( + data: dict, + expires_delta: Optional[timedelta] = None +) -> str: + """ + Crear token JWT de acceso. + + Args: + data: Datos a incluir en el token (user_id, email, etc.) + expires_delta: Tiempo de expiración custom + + Returns: + Token JWT codificado + """ + to_encode = data.copy() + + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + + to_encode.update({"exp": expire, "type": "access"}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + return encoded_jwt + + +def create_refresh_token(data: dict) -> str: + """Crear token JWT de refresh (larga duración)""" + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) + to_encode.update({"exp": expire, "type": "refresh"}) + + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_token(token: str) -> TokenData: + """ + Decodificar y validar token JWT. + + Args: + token: Token JWT a decodificar + + Returns: + Datos del token + + Raises: + JWTError: Si el token es inválido o expiró + """ + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + email: str = payload.get("email") + + if user_id is None: + raise JWTError("Token inválido: falta user_id") + + return TokenData(user_id=user_id, email=email) + + except JWTError as e: + raise JWTError(f"Error al decodificar token: {str(e)}") +``` + +### `backend/app/api/v1/auth.py` + +```python +""" +Endpoints de autenticación: login, registro, refresh token. +""" +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from pydantic import BaseModel, EmailStr + +from app.core.security import ( + verify_password, + get_password_hash, + create_access_token, + create_refresh_token, + decode_token +) +from app.db.session import get_db +from app.models.user import User + + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + + +class UserRegister(BaseModel): + """Schema para registro de usuario""" + email: EmailStr + password: str + name: str + + +class Token(BaseModel): + """Schema de respuesta de autenticación""" + access_token: str + refresh_token: str + token_type: str = "bearer" + + +@router.post("/register", response_model=Token, status_code=status.HTTP_201_CREATED) +async def register(user_data: UserRegister, db: Session = Depends(get_db)): + """ + Registrar nuevo usuario. + + - Valida que el email no exista + - Hashea el password + - Crea usuario en BD + - Retorna tokens de acceso + """ + # Verificar que el email no exista + existing_user = db.query(User).filter(User.email == user_data.email).first() + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email ya registrado" + ) + + # Crear usuario + new_user = User( + email=user_data.email, + password_hash=get_password_hash(user_data.password), + name=user_data.name + ) + + db.add(new_user) + db.commit() + db.refresh(new_user) + + # Generar tokens + access_token = create_access_token(data={"sub": str(new_user.id), "email": new_user.email}) + refresh_token = create_refresh_token(data={"sub": str(new_user.id), "email": new_user.email}) + + return Token(access_token=access_token, refresh_token=refresh_token) + + +@router.post("/login", response_model=Token) +async def login( + form_data: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db) +): + """ + Login de usuario. + + - Valida credenciales + - Retorna tokens de acceso + """ + # Buscar usuario por email + user = db.query(User).filter(User.email == form_data.username).first() + + if not user or not verify_password(form_data.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Credenciales incorrectas", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Generar tokens + access_token = create_access_token(data={"sub": str(user.id), "email": user.email}) + refresh_token = create_refresh_token(data={"sub": str(user.id), "email": user.email}) + + return Token(access_token=access_token, refresh_token=refresh_token) + + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: Session = Depends(get_db) +) -> User: + """ + Dependency para obtener usuario actual desde token JWT. + Usar en endpoints protegidos: `user: User = Depends(get_current_user)` + """ + try: + token_data = decode_token(token) + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token inválido o expirado", + headers={"WWW-Authenticate": "Bearer"}, + ) + + user = db.query(User).filter(User.id == token_data.user_id).first() + + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Usuario no encontrado" + ) + + return user +``` + +--- + +**(Continuará en el siguiente archivo con ejemplos de Frontend...)** + +**Documento creado**: 2026-01-02 +**Parte**: 1/2 diff --git a/NUTRITION_FRONTEND_EXAMPLES.md b/NUTRITION_FRONTEND_EXAMPLES.md new file mode 100644 index 0000000..79fdbd9 --- /dev/null +++ b/NUTRITION_FRONTEND_EXAMPLES.md @@ -0,0 +1,1036 @@ +# 💻 Ejemplos de Frontend - Aplicación de Nutrición + +## Índice - Parte 2 +1. [Dashboard de Calorías (React)](#dashboard-calorias) +2. [Upload de Fotos](#upload-fotos) +3. [Formulario de Comidas](#formulario-comidas) +4. [Gráficos de Progreso](#graficos-progreso) +5. [Plan Semanal](#plan-semanal) +6. [Hooks Personalizados](#hooks-personalizados) +7. [Servicios API](#servicios-api) + +--- + +## 1️⃣ Dashboard de Calorías (React) + +### `frontend/src/pages/Dashboard.tsx` + +```typescript +/** + * Dashboard principal con resumen nutricional del día + */ +import React, { useEffect } from 'react'; +import { useMeals } from '../hooks/useMeals'; +import { useUser } from '../hooks/useUser'; +import CaloriesProgress from '../components/dashboard/CaloriesProgress'; +import MacrosChart from '../components/dashboard/MacrosChart'; +import DailyMeals from '../components/dashboard/DailyMeals'; +import { format } from 'date-fns'; +import { es } from 'date-fns/locale'; + +export default function Dashboard() { + const { user } = useUser(); + const { meals, isLoading, fetchMealsByDate } = useMeals(); + const today = new Date(); + + useEffect(() => { + fetchMealsByDate(today); + }, []); + + if (isLoading) { + return ( +
+
+
+ ); + } + + // Calcular totales del día + const totals = meals.reduce( + (acc, meal) => ({ + calories: acc.calories + meal.total_calories, + protein: acc.protein + meal.protein_g, + carbs: acc.carbs + meal.carbs_g, + fat: acc.fat + meal.fat_g, + }), + { calories: 0, protein: 0, carbs: 0, fat: 0 } + ); + + // Obtener objetivo del usuario + const targetCalories = user?.target_calories || 2000; + const targetMacros = user?.macros || { + protein_g: 150, + carbs_g: 200, + fat_g: 65, + }; + + return ( +
+ {/* Header */} +
+
+

+ Hola, {user?.name} 👋 +

+

+ {format(today, "EEEE, d 'de' MMMM", { locale: es })} +

+
+
+ +
+ {/* Progreso de calorías */} + + + {/* Macronutrientes */} + + + {/* Comidas del día */} + + + {/* Botón agregar comida */} + +
+
+ ); +} +``` + +### `frontend/src/components/dashboard/CaloriesProgress.tsx` + +```typescript +/** + * Componente de progreso de calorías con barra visual + */ +import React from 'react'; + +interface Props { + current: number; + target: number; +} + +export default function CaloriesProgress({ current, target }: Props) { + const percentage = Math.min((current / target) * 100, 100); + const remaining = Math.max(target - current, 0); + + // Determinar color según progreso + let colorClass = 'bg-green-500'; + if (percentage > 100) colorClass = 'bg-red-500'; + else if (percentage > 90) colorClass = 'bg-yellow-500'; + + return ( +
+
+
+

+ {Math.round(current)} / {target} cal +

+

+ {remaining > 0 ? `Quedan ${Math.round(remaining)} calorías` : 'Objetivo alcanzado!'} +

+
+
+ {percentage < 50 ? '😋' : percentage < 90 ? '😊' : percentage <= 100 ? '✅' : '🚫'} +
+
+ + {/* Barra de progreso */} +
+
+
+ + {/* Porcentaje */} +
+ {Math.round(percentage)}% +
+
+ ); +} +``` + +### `frontend/src/components/dashboard/MacrosChart.tsx` + +```typescript +/** + * Gráfico de macronutrientes (proteína, carbos, grasas) + */ +import React from 'react'; + +interface Macros { + protein: number; + carbs: number; + fat: number; +} + +interface Props { + current: Macros; + target: Macros; +} + +export default function MacrosChart({ current, target }: Props) { + const macros = [ + { + name: 'Proteína', + current: current.protein, + target: target.protein_g, + color: 'bg-blue-500', + emoji: '🥩', + }, + { + name: 'Carbohidratos', + current: current.carbs, + target: target.carbs_g, + color: 'bg-yellow-500', + emoji: '🍞', + }, + { + name: 'Grasas', + current: current.fat, + target: target.fat_g, + color: 'bg-purple-500', + emoji: '🥑', + }, + ]; + + return ( +
+

+ Macronutrientes +

+ +
+ {macros.map((macro) => { + const percentage = Math.min((macro.current / macro.target) * 100, 100); + + return ( +
+
+
+ {macro.emoji} + {macro.name} +
+ + {Math.round(macro.current)}g / {macro.target}g + +
+ + {/* Barra de progreso */} +
+
+
+
+ ); + })} +
+
+ ); +} +``` + +--- + +## 2️⃣ Upload de Fotos + +### `frontend/src/components/meals/PhotoUpload.tsx` + +```typescript +/** + * Componente para subir fotos de comidas y analizarlas con IA + */ +import React, { useState, useRef } from 'react'; +import { mealService } from '../../services/mealService'; + +interface Props { + onAnalysisComplete: (analysis: any) => void; +} + +export default function PhotoUpload({ onAnalysisComplete }: Props) { + const [selectedFile, setSelectedFile] = useState(null); + const [preview, setPreview] = useState(null); + const [isAnalyzing, setIsAnalyzing] = useState(false); + const [error, setError] = useState(null); + const fileInputRef = useRef(null); + + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + // Validar tipo de archivo + if (!file.type.startsWith('image/')) { + setError('Por favor selecciona una imagen válida'); + return; + } + + // Validar tamaño (max 10MB) + if (file.size > 10 * 1024 * 1024) { + setError('La imagen es muy grande. Máximo 10MB'); + return; + } + + setSelectedFile(file); + setError(null); + + // Crear preview + const reader = new FileReader(); + reader.onloadend = () => { + setPreview(reader.result as string); + }; + reader.readAsDataURL(file); + }; + + const handleAnalyze = async () => { + if (!selectedFile) return; + + setIsAnalyzing(true); + setError(null); + + try { + // Subir foto y obtener análisis + const analysis = await mealService.analyzePhoto(selectedFile); + onAnalysisComplete(analysis); + } catch (err: any) { + setError(err.message || 'Error al analizar la foto'); + } finally { + setIsAnalyzing(false); + } + }; + + const handleClear = () => { + setSelectedFile(null); + setPreview(null); + setError(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ {/* Área de upload */} +
fileInputRef.current?.click()} + className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-green-500 transition" + > + {preview ? ( +
+ Preview + +
+ ) : ( +
+ + + +

+ Haz click para subir o arrastra una foto +

+

PNG, JPG hasta 10MB

+
+ )} +
+ + + + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Botón analizar */} + {selectedFile && ( + + )} +
+ ); +} +``` + +--- + +## 3️⃣ Formulario de Comidas + +### `frontend/src/pages/AddMeal.tsx` + +```typescript +/** + * Página para agregar comidas (foto, descripción o manual) + */ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import PhotoUpload from '../components/meals/PhotoUpload'; +import RecipeInput from '../components/meals/RecipeInput'; +import ManualEntry from '../components/meals/ManualEntry'; +import { mealService } from '../services/mealService'; + +type MealType = 'breakfast' | 'lunch' | 'snack' | 'dinner'; + +export default function AddMeal() { + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState<'photo' | 'recipe' | 'manual'>('photo'); + const [mealType, setMealType] = useState('lunch'); + const [analysis, setAnalysis] = useState(null); + const [isSaving, setIsSaving] = useState(false); + + const tabs = [ + { id: 'photo', label: '📸 Foto', icon: '📷' }, + { id: 'recipe', label: '📝 Descripción', icon: '✍️' }, + { id: 'manual', label: '⌨️ Manual', icon: '📋' }, + ]; + + const mealTypes = [ + { id: 'breakfast', label: 'Desayuno', icon: '🌅' }, + { id: 'lunch', label: 'Almuerzo', icon: '☀️' }, + { id: 'snack', label: 'Merienda', icon: '🍎' }, + { id: 'dinner', label: 'Cena', icon: '🌙' }, + ]; + + const handleSaveMeal = async () => { + if (!analysis) return; + + setIsSaving(true); + try { + await mealService.createMeal({ + meal_type: mealType, + date: new Date().toISOString().split('T')[0], + ...analysis, + }); + + // Redirigir al dashboard + navigate('/dashboard'); + } catch (error) { + console.error('Error al guardar comida:', error); + alert('Error al guardar la comida'); + } finally { + setIsSaving(false); + } + }; + + return ( +
+ {/* Header */} +
+
+ +

+ Agregar Comida +

+
+
+ +
+ {/* Selector de tipo de comida */} +
+

Tipo de comida

+
+ {mealTypes.map((type) => ( + + ))} +
+
+ + {/* Tabs */} +
+
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {activeTab === 'photo' && ( + + )} + {activeTab === 'recipe' && ( + + )} + {activeTab === 'manual' && ( + + )} +
+
+ + {/* Resumen del análisis */} + {analysis && ( +
+

+ Resumen Nutricional +

+ +
+
+
+ {Math.round(analysis.total_calories)} +
+
Calorías
+
+
+
+ {Math.round(analysis.total_protein_g)}g +
+
Proteína
+
+
+
+ {Math.round(analysis.total_carbs_g)}g +
+
Carbos
+
+
+
+ {Math.round(analysis.total_fat_g)}g +
+
Grasas
+
+
+ + {/* Lista de alimentos */} + {analysis.foods && ( +
+

Alimentos detectados:

+
+ {analysis.foods.map((food: any, index: number) => ( +
+ + {food.name} ({food.quantity}{food.unit}) + + + {Math.round(food.calories)} cal + +
+ ))} +
+
+ )} + + {/* Botón guardar */} + +
+ )} +
+
+ ); +} +``` + +--- + +## 4️⃣ Gráficos de Progreso + +### `frontend/src/components/progress/WeightChart.tsx` + +```typescript +/** + * Gráfico de evolución del peso + */ +import React from 'react'; +import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; +import { format, parseISO } from 'date-fns'; +import { es } from 'date-fns/locale'; + +interface WeightLog { + date: string; + weight_kg: number; +} + +interface Props { + data: WeightLog[]; + targetWeight: number; +} + +export default function WeightChart({ data, targetWeight }: Props) { + // Formatear datos para el gráfico + const chartData = data.map((log) => ({ + date: format(parseISO(log.date), 'dd/MM', { locale: es }), + peso: log.weight_kg, + objetivo: targetWeight, + })); + + return ( +
+

+ Evolución del Peso +

+ + + + + + + + + + + + + {/* Estadísticas */} +
+
+
Peso inicial
+
+ {data[0]?.weight_kg.toFixed(1)} kg +
+
+
+
Peso actual
+
+ {data[data.length - 1]?.weight_kg.toFixed(1)} kg +
+
+
+
Diferencia
+
+ {(data[0]?.weight_kg - data[data.length - 1]?.weight_kg).toFixed(1)} kg +
+
+
+
+ ); +} +``` + +--- + +## 5️⃣ Hooks Personalizados + +### `frontend/src/hooks/useMeals.ts` + +```typescript +/** + * Hook para gestionar comidas + */ +import { useState, useCallback } from 'react'; +import { mealService } from '../services/mealService'; + +interface Meal { + id: string; + date: string; + meal_type: string; + name: string; + total_calories: number; + protein_g: number; + carbs_g: number; + fat_g: number; +} + +export function useMeals() { + const [meals, setMeals] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const fetchMealsByDate = useCallback(async (date: Date) => { + setIsLoading(true); + setError(null); + + try { + const dateStr = date.toISOString().split('T')[0]; + const data = await mealService.getMealsByDate(dateStr); + setMeals(data); + } catch (err: any) { + setError(err.message || 'Error al cargar comidas'); + setMeals([]); + } finally { + setIsLoading(false); + } + }, []); + + const createMeal = useCallback(async (mealData: Partial) => { + setIsLoading(true); + setError(null); + + try { + const newMeal = await mealService.createMeal(mealData); + setMeals((prev) => [...prev, newMeal]); + return newMeal; + } catch (err: any) { + setError(err.message || 'Error al crear comida'); + throw err; + } finally { + setIsLoading(false); + } + }, []); + + const deleteMeal = useCallback(async (mealId: string) => { + setIsLoading(true); + setError(null); + + try { + await mealService.deleteMeal(mealId); + setMeals((prev) => prev.filter((m) => m.id !== mealId)); + } catch (err: any) { + setError(err.message || 'Error al eliminar comida'); + throw err; + } finally { + setIsLoading(false); + } + }, []); + + return { + meals, + isLoading, + error, + fetchMealsByDate, + createMeal, + deleteMeal, + }; +} +``` + +--- + +## 6️⃣ Servicios API + +### `frontend/src/services/api.ts` + +```typescript +/** + * Cliente HTTP configurado con Axios + */ +import axios from 'axios'; + +const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/v1'; + +// Crear instancia de Axios +export const api = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Interceptor para agregar token JWT +api.interceptors.request.use( + (config) => { + const token = localStorage.getItem('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +// Interceptor para manejar errores de autenticación +api.interceptors.response.use( + (response) => response, + async (error) => { + if (error.response?.status === 401) { + // Token expirado, intentar refresh + const refreshToken = localStorage.getItem('refresh_token'); + + if (refreshToken) { + try { + const response = await axios.post(`${API_URL}/auth/refresh`, { + refresh_token: refreshToken, + }); + + const { access_token } = response.data; + localStorage.setItem('access_token', access_token); + + // Reintentar request original + error.config.headers.Authorization = `Bearer ${access_token}`; + return axios(error.config); + } catch { + // Refresh falló, redirigir a login + localStorage.clear(); + window.location.href = '/login'; + } + } else { + // No hay refresh token, redirigir a login + localStorage.clear(); + window.location.href = '/login'; + } + } + + return Promise.reject(error); + } +); + +export default api; +``` + +### `frontend/src/services/mealService.ts` + +```typescript +/** + * Servicio para operaciones con comidas + */ +import api from './api'; + +interface MealData { + meal_type: string; + date: string; + name?: string; + description?: string; + total_calories: number; + protein_g: number; + carbs_g: number; + fat_g: number; + foods?: any[]; +} + +export const mealService = { + /** + * Obtener comidas por fecha + */ + async getMealsByDate(date: string) { + const response = await api.get(`/meals`, { + params: { date }, + }); + return response.data; + }, + + /** + * Crear nueva comida + */ + async createMeal(data: Partial) { + const response = await api.post('/meals', data); + return response.data; + }, + + /** + * Analizar foto de comida + */ + async analyzePhoto(file: File, notes: string = '') { + const formData = new FormData(); + formData.append('photo', file); + formData.append('notes', notes); + + const response = await api.post('/photos/analyze', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + return response.data; + }, + + /** + * Analizar receta en texto + */ + async analyzeRecipe(description: string) { + const response = await api.post('/recipes/parse', { + description, + }); + return response.data; + }, + + /** + * Eliminar comida + */ + async deleteMeal(mealId: string) { + await api.delete(`/meals/${mealId}`); + }, +}; +``` + +--- + +## 7️⃣ Store con Zustand + +### `frontend/src/store/userStore.ts` + +```typescript +/** + * Store global para datos de usuario + */ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface User { + id: string; + email: string; + name: string; + target_calories: number; + macros: { + protein_g: number; + carbs_g: number; + fat_g: number; + }; + goal_type: string; +} + +interface UserStore { + user: User | null; + isAuthenticated: boolean; + setUser: (user: User) => void; + logout: () => void; +} + +export const useUserStore = create()( + persist( + (set) => ({ + user: null, + isAuthenticated: false, + + setUser: (user) => + set({ + user, + isAuthenticated: true, + }), + + logout: () => { + localStorage.clear(); + set({ + user: null, + isAuthenticated: false, + }); + }, + }), + { + name: 'user-storage', + } + ) +); +``` + +--- + +## 🎯 Resumen de Componentes + +### Componentes Creados +- ✅ `Dashboard.tsx` - Pantalla principal +- ✅ `CaloriesProgress.tsx` - Barra de progreso de calorías +- ✅ `MacrosChart.tsx` - Gráfico de macronutrientes +- ✅ `PhotoUpload.tsx` - Upload y análisis de fotos +- ✅ `AddMeal.tsx` - Formulario de agregar comidas +- ✅ `WeightChart.tsx` - Gráfico de evolución de peso +- ✅ `useMeals.ts` - Hook de comidas +- ✅ `api.ts` - Cliente HTTP con interceptors +- ✅ `mealService.ts` - Servicio de comidas +- ✅ `userStore.ts` - Store global de usuario + +### Próximos Pasos +1. Implementar componente de plan semanal +2. Agregar notificaciones push +3. Crear página de configuración +4. Implementar modo offline +5. Agregar tests unitarios + +--- + +**Documento creado**: 2026-01-02 +**Versión**: 1.0 diff --git a/NUTRITION_README.md b/NUTRITION_README.md new file mode 100644 index 0000000..c285e27 --- /dev/null +++ b/NUTRITION_README.md @@ -0,0 +1,441 @@ +# 🥗 Nutrition Tracker - Aplicación de Seguimiento Nutricional + +> Aplicación completa para seguimiento de alimentación, análisis nutricional con IA y generación de planes personalizados. + +--- + +## 📋 Resumen Ejecutivo + +**Nutrition Tracker** es una aplicación web y móvil que permite a los usuarios: + +- 📸 **Registrar comidas** mediante fotos, descripciones o entrada manual +- 🤖 **Análisis automático con IA** usando GPT-4 Vision para reconocer alimentos +- 📊 **Cálculo preciso** de calorías y macronutrientes +- 🎯 **Planes personalizados** según objetivos (pérdida de peso, detox, ganancia muscular) +- 📈 **Seguimiento de progreso** con gráficos y métricas +- 📅 **Planes semanales** con recetas y listas de compras automáticas + +--- + +## 🎯 Objetivos Soportados + +| Objetivo | Descripción | Estrategia Nutricional | +|----------|-------------|------------------------| +| **Pérdida de peso** | Déficit calórico controlado | -500 cal/día, 30% proteína | +| **Detox** | Alimentación limpia y antiinflamatoria | Mantenimiento, alimentos integrales | +| **Quema de grasa basal** | Optimización metabólica | -300 cal/día, balance de macros | +| **Mantenimiento** | Equilibrio nutricional | TDEE exacto, 25/45/30 macros | +| **Ganancia muscular** | Superávit con alta proteína | +300 cal/día, 35% proteína | + +--- + +## ✨ Características Principales + +### 1. Perfil Personalizado +- Cálculo automático de necesidades calóricas (TMB + TDEE) +- Ajuste por edad, sexo, peso, altura y nivel de actividad +- Distribución óptima de macronutrientes según objetivo + +### 2. Registro Inteligente de Comidas +- **Por foto**: Sube una imagen y la IA identifica automáticamente los alimentos +- **Por descripción**: Escribe "ensalada césar con pollo" y obtén el análisis nutricional +- **Manual**: Busca alimentos en base de datos USDA (390,000+ alimentos) + +### 3. Dashboard Interactivo +- Progreso diario de calorías y macros en tiempo real +- Gráficos visuales de proteínas, carbohidratos y grasas +- Historial de comidas del día +- Indicadores de cumplimiento de objetivos + +### 4. Generador de Planes Semanales +- Sugerencias automáticas de comidas según tu objetivo +- Recetas con instrucciones paso a paso +- Lista de compras consolidada +- Consideración de preferencias (vegano, sin gluten, etc.) + +### 5. Seguimiento de Progreso +- Gráficos de evolución de peso +- Tendencias de consumo calórico +- Racha de días consecutivos +- Logros y badges gamificados + +--- + +## 🏗️ Arquitectura Técnica + +### Stack Tecnológico + +``` +Backend: FastAPI + Python 3.10+ +Frontend: React + TypeScript + Tailwind CSS +Mobile: React Native (futuro) +Database: PostgreSQL + Redis +IA: OpenAI GPT-4 Vision +APIs: USDA FoodData Central (gratis) + Spoonacular (recetas) +Hosting: Railway / Render (backend) + Vercel (frontend) +``` + +### Componentes Principales + +``` +nutrition-tracker/ +├── backend/ # API REST con FastAPI +│ ├── app/ +│ │ ├── api/v1/ # Endpoints +│ │ ├── models/ # Modelos SQLAlchemy +│ │ ├── services/ # Lógica de negocio +│ │ │ ├── nutrition_calc.py # Cálculos TMB/TDEE +│ │ │ ├── ai_vision.py # Análisis de fotos +│ │ │ ├── usda_api.py # Integración USDA +│ │ │ └── plan_generator.py # Planes semanales +│ │ └── schemas/ # Validación Pydantic +│ └── alembic/ # Migraciones de BD +│ +├── frontend/ # Aplicación React +│ ├── src/ +│ │ ├── components/ # Componentes reutilizables +│ │ ├── pages/ # Pantallas principales +│ │ ├── hooks/ # Custom hooks +│ │ ├── services/ # Servicios API +│ │ └── store/ # Estado global (Zustand) +│ └── public/ +│ +└── docs/ # Documentación del proyecto +``` + +--- + +## 📊 Modelo de Datos + +### Entidades Principales + +1. **users** - Perfiles de usuario con objetivos y preferencias +2. **meals** - Comidas registradas con análisis nutricional +3. **food_items** - Items individuales dentro de cada comida +4. **recipes** - Recetas con ingredientes e instrucciones +5. **weekly_plans** - Planes semanales generados +6. **weight_logs** - Registro histórico de peso + +### Relaciones + +``` +User (1) ──< (N) Meals ──< (N) FoodItems +User (1) ──< (N) WeeklyPlans +User (1) ──< (N) WeightLogs +WeeklyPlan (N) ──< (N) Recipes +``` + +--- + +## 🚀 Inicio Rápido + +### Prerrequisitos + +- Python 3.10+ +- Node.js 18+ +- PostgreSQL 14+ +- Cuenta OpenAI (para análisis de fotos) + +### Instalación en 5 pasos + +```bash +# 1. Clonar repositorio +git clone https://github.com/tu-usuario/nutrition-tracker.git +cd nutrition-tracker + +# 2. Configurar backend +cd backend +python -m venv venv +source venv/bin/activate # o venv\Scripts\activate en Windows +pip install -r requirements.txt + +# 3. Configurar base de datos +createdb nutrition_db +cp .env.example .env +# Editar .env con tus credenciales + +# 4. Ejecutar migraciones +alembic upgrade head + +# 5. Iniciar aplicación +# Terminal 1 - Backend +uvicorn app.main:app --reload + +# Terminal 2 - Frontend +cd ../frontend +npm install +npm start +``` + +Abre http://localhost:3000 en tu navegador. + +--- + +## 📖 Documentación Completa + +Este repositorio incluye documentación detallada: + +| Documento | Descripción | +|-----------|-------------| +| **[NUTRITION_APP_PLAN.md](NUTRITION_APP_PLAN.md)** | Plan completo de la aplicación con arquitectura, funcionalidades y roadmap | +| **[NUTRITION_APP_STRUCTURE.md](NUTRITION_APP_STRUCTURE.md)** | Estructura de directorios, esquema de BD y comandos de desarrollo | +| **[NUTRITION_CODE_EXAMPLES.md](NUTRITION_CODE_EXAMPLES.md)** | Ejemplos de código backend (Python/FastAPI) | +| **[NUTRITION_FRONTEND_EXAMPLES.md](NUTRITION_FRONTEND_EXAMPLES.md)** | Ejemplos de código frontend (React/TypeScript) | +| **[NUTRITION_SETUP_GUIDE.md](NUTRITION_SETUP_GUIDE.md)** | Guía paso a paso de instalación y configuración | + +--- + +## 💡 Casos de Uso + +### Caso 1: Usuario Principiante + +María, 28 años, quiere perder 5kg. Nunca ha contado calorías. + +1. Se registra y completa perfil (edad, peso, altura, actividad) +2. La app calcula: necesita 1,600 cal/día con 30% proteína +3. En el almuerzo, saca foto de su ensalada +4. La IA reconoce: lechuga, tomate, pollo, aderezo → 380 calorías +5. El dashboard muestra: 380/1,600 cal consumidas (23%) +6. Al final del día, revisa si cumplió su objetivo + +### Caso 2: Usuario Avanzado + +Carlos, 35 años, culturista, necesita plan semanal optimizado. + +1. Configura objetivo: ganancia muscular (2,400 cal, 35% proteína) +2. Genera plan semanal automático +3. Recibe 21 recetas (7 días × 3 comidas) optimizadas +4. Descarga lista de compras para la semana +5. Cada día, marca las comidas como completadas +6. Revisa gráficos de tendencia de peso y macros + +### Caso 3: Usuario con Restricciones + +Ana, vegana, intolerante al gluten, quiere plan detox. + +1. Configura preferencias: vegano + sin gluten +2. Selecciona objetivo: detox (alimentos antiinflamatorios) +3. El generador filtra recetas automáticamente +4. Recibe plan personalizado con solo opciones válidas +5. Todas las recetas son veganas y sin gluten +6. Puede solicitar variaciones con un click + +--- + +## 🧮 Cálculos Nutricionales + +### Tasa Metabólica Basal (TMB) + +Fórmula Mifflin-St Jeor: + +``` +Hombres: TMB = (10 × peso) + (6.25 × altura) - (5 × edad) + 5 +Mujeres: TMB = (10 × peso) + (6.25 × altura) - (5 × edad) - 161 +``` + +### Gasto Energético Total (TDEE) + +``` +TDEE = TMB × Factor de Actividad + +Factores: +- Sedentario: 1.2 (poco o ningún ejercicio) +- Ligero: 1.375 (ejercicio 1-3 días/semana) +- Moderado: 1.55 (ejercicio 3-5 días/semana) +- Activo: 1.725 (ejercicio 6-7 días/semana) +- Muy activo: 1.9 (ejercicio intenso + trabajo físico) +``` + +### Distribución de Macronutrientes + +| Objetivo | Proteína | Carbos | Grasas | +|----------|----------|--------|--------| +| Pérdida de peso | 30% | 35% | 35% | +| Detox | 25% | 45% | 30% | +| Mantenimiento | 25% | 45% | 30% | +| Ganancia muscular | 35% | 45% | 20% | + +--- + +## 🤖 Análisis con IA + +### Flujo de Procesamiento de Fotos + +``` +1. Usuario sube foto de comida + ↓ +2. Validación (formato, tamaño) + ↓ +3. Envío a GPT-4 Vision API + ↓ +4. IA identifica alimentos y estima porciones + ↓ +5. Respuesta JSON con lista de alimentos + ↓ +6. Cálculo de calorías y macros + ↓ +7. Almacenamiento en base de datos +``` + +### Ejemplo de Respuesta de IA + +```json +{ + "description": "Plato de arroz con pollo y ensalada", + "foods": [ + { + "name": "pechuga de pollo a la plancha", + "quantity": 150, + "unit": "g", + "calories": 247, + "protein_g": 46.5, + "carbs_g": 0, + "fat_g": 5.4, + "confidence": 0.85 + }, + { + "name": "arroz blanco cocido", + "quantity": 200, + "unit": "g", + "calories": 260, + "protein_g": 5.3, + "carbs_g": 57, + "fat_g": 0.6, + "confidence": 0.90 + } + ] +} +``` + +--- + +## 💰 Costos Estimados + +### Desarrollo +- **Gratis** (proyecto open source) + +### Infraestructura (1,000 usuarios activos/mes) +``` +Backend (Railway): $15/mes +PostgreSQL: $5/mes +Redis (Upstash): Gratis +Storage (Cloudflare R2): $2/mes +────────────────────────────────── +Subtotal: $22/mes +``` + +### APIs Externas +``` +USDA FoodData: Gratis (ilimitado) +OpenAI GPT-4 Vision: $30/mes (100 fotos/día) +Spoonacular API: $49/mes (plan básico) +────────────────────────────────── +Subtotal: $79/mes +``` + +**Total: ~$100/mes** para MVP con 1,000 usuarios activos + +### Escalamiento + +Para 10,000 usuarios activos: ~$300-400/mes + +--- + +## 📈 Roadmap de Desarrollo + +### Fase 1: MVP (4 semanas) ✅ EN PLANIFICACIÓN +- [x] Backend básico (FastAPI + PostgreSQL) +- [x] Autenticación de usuarios (JWT) +- [x] Cálculo de necesidades calóricas +- [x] Registro manual de comidas +- [x] Dashboard con calorías y macros +- [x] Integración con USDA API + +### Fase 2: IA de Fotos (2 semanas) +- [ ] Integración con GPT-4 Vision +- [ ] Upload de fotos +- [ ] Análisis automático de alimentos +- [ ] Edición de resultados + +### Fase 3: Parser de Recetas (2 semanas) +- [ ] NLP para procesar descripciones +- [ ] Extracción de ingredientes +- [ ] Cálculo nutricional de recetas + +### Fase 4: Planes Semanales (3 semanas) +- [ ] Motor de recomendaciones +- [ ] Integración con Spoonacular +- [ ] Generador de listas de compras +- [ ] Algoritmo de optimización + +### Fase 5: Frontend Web (3 semanas) +- [ ] React + TypeScript +- [ ] Todas las pantallas principales +- [ ] Gráficos de progreso +- [ ] Responsive design + +### Fase 6: App Móvil (4 semanas) +- [ ] React Native +- [ ] Cámara integrada +- [ ] Notificaciones push +- [ ] Modo offline + +--- + +## 🤝 Contribuir + +Contribuciones son bienvenidas! Por favor: + +1. Fork el proyecto +2. Crea una rama (`git checkout -b feature/nueva-funcionalidad`) +3. Commit tus cambios (`git commit -m 'Agregar nueva funcionalidad'`) +4. Push a la rama (`git push origin feature/nueva-funcionalidad`) +5. Abre un Pull Request + +--- + +## 📄 Licencia + +Este proyecto está bajo la licencia MIT. Ver archivo `LICENSE` para más detalles. + +--- + +## 📧 Contacto + +Para preguntas, sugerencias o reportar bugs: + +- **GitHub Issues**: [github.com/tu-usuario/nutrition-tracker/issues](https://github.com/tu-usuario/nutrition-tracker/issues) +- **Email**: tu-email@ejemplo.com + +--- + +## 🙏 Agradecimientos + +- **USDA FoodData Central** - Base de datos de alimentos gratuita +- **OpenAI** - GPT-4 Vision API +- **Spoonacular** - API de recetas +- **FastAPI** - Framework web moderno +- **React** - Biblioteca de UI + +--- + +## 📊 Estado del Proyecto + +``` +┌─────────────────────────────────────────┐ +│ Estado: 📝 EN PLANIFICACIÓN │ +│ Fase actual: Fase 1 - MVP │ +│ Progreso: ████░░░░░░░░░░░░░░░ 25% │ +│ Próxima release: v0.1.0 (Beta MVP) │ +└─────────────────────────────────────────┘ +``` + +**Última actualización:** 2026-01-02 + +--- + +**¡Gracias por tu interés en Nutrition Tracker! 🥗** + +Si este proyecto te resulta útil, considera darle una ⭐ en GitHub. diff --git a/NUTRITION_SETUP_GUIDE.md b/NUTRITION_SETUP_GUIDE.md new file mode 100644 index 0000000..ccb4d4c --- /dev/null +++ b/NUTRITION_SETUP_GUIDE.md @@ -0,0 +1,876 @@ +# 🚀 Guía de Setup - Aplicación de Nutrición + +## Inicio Rápido - Checklist + +- [ ] Configurar entorno de desarrollo +- [ ] Crear estructura de directorios +- [ ] Configurar base de datos PostgreSQL +- [ ] Instalar dependencias backend +- [ ] Configurar variables de entorno +- [ ] Ejecutar migraciones +- [ ] Instalar dependencias frontend +- [ ] Configurar APIs externas +- [ ] Probar aplicación localmente + +**Tiempo estimado:** 1-2 horas + +--- + +## 1️⃣ Prerrequisitos + +### Software Requerido + +```bash +# Verificar versiones instaladas +python --version # Python 3.10 o superior +node --version # Node.js 18 o superior +npm --version # npm 9 o superior +psql --version # PostgreSQL 14 o superior +git --version # Git (cualquier versión reciente) +``` + +### Instalación de Software Faltante + +#### macOS (Homebrew) +```bash +brew install python@3.10 +brew install node +brew install postgresql@14 +brew install git +``` + +#### Ubuntu/Debian +```bash +sudo apt update +sudo apt install python3.10 python3.10-venv python3-pip +sudo apt install nodejs npm +sudo apt install postgresql postgresql-contrib +sudo apt install git +``` + +#### Windows +```powershell +# Usar Chocolatey +choco install python --version=3.10 +choco install nodejs +choco install postgresql14 +choco install git + +# O descargar instaladores: +# Python: https://www.python.org/downloads/ +# Node.js: https://nodejs.org/ +# PostgreSQL: https://www.postgresql.org/download/windows/ +# Git: https://git-scm.com/download/win +``` + +--- + +## 2️⃣ Crear Estructura del Proyecto + +### Opción A: Clonar estructura base (recomendado) + +```bash +# Crear directorio del proyecto +mkdir nutrition-tracker +cd nutrition-tracker + +# Crear estructura de directorios +mkdir -p backend/app/{api/v1,core,db,models,schemas,services,utils} +mkdir -p backend/alembic/versions +mkdir -p backend/tests +mkdir -p frontend/src/{components/{auth,dashboard,meals,plans,profile,progress,common},hooks,services,store,types,utils,pages} +mkdir -p frontend/public +mkdir -p docs +mkdir -p scripts + +# Inicializar Git +git init +echo "# Nutrition Tracker App" > README.md +git add README.md +git commit -m "Initial commit" +``` + +### Opción B: Script automatizado + +```bash +# Guardar como setup_structure.sh +cat > setup_structure.sh << 'EOF' +#!/bin/bash +echo "🚀 Creando estructura del proyecto..." + +# Backend +mkdir -p backend/app/{api/v1,core,db,models,schemas,services,utils} +mkdir -p backend/alembic/versions +mkdir -p backend/tests + +# Frontend +mkdir -p frontend/src/{components/{auth,dashboard,meals,plans,profile,progress,common},hooks,services,store,types,utils,pages} +mkdir -p frontend/public + +# Otros +mkdir -p docs scripts + +echo "✅ Estructura creada exitosamente!" +tree -L 3 -d +EOF + +chmod +x setup_structure.sh +./setup_structure.sh +``` + +--- + +## 3️⃣ Configurar Backend (FastAPI) + +### Paso 1: Crear entorno virtual + +```bash +cd backend + +# Crear entorno virtual +python3 -m venv venv + +# Activar entorno virtual +# En macOS/Linux: +source venv/bin/activate + +# En Windows: +venv\Scripts\activate + +# Verificar que está activado +which python # Debe mostrar ruta dentro de venv/ +``` + +### Paso 2: Instalar dependencias + +```bash +# Crear requirements.txt +cat > requirements.txt << 'EOF' +# Framework +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +pydantic==2.5.0 +pydantic-settings==2.1.0 + +# Database +sqlalchemy==2.0.25 +alembic==1.13.1 +psycopg2-binary==2.9.9 +asyncpg==0.29.0 + +# Authentication +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 + +# External APIs +openai==1.12.0 +httpx==0.26.0 +aiohttp==3.9.1 + +# Utilities +python-dotenv==1.0.0 +pillow==10.2.0 +redis==5.0.1 + +# Testing +pytest==7.4.4 +pytest-asyncio==0.23.3 +pytest-cov==4.1.0 +EOF + +# Instalar dependencias +pip install -r requirements.txt + +# Verificar instalación +pip list +``` + +### Paso 3: Configurar base de datos PostgreSQL + +```bash +# Iniciar servicio PostgreSQL +# macOS/Linux: +sudo service postgresql start + +# macOS (Homebrew): +brew services start postgresql@14 + +# Crear base de datos y usuario +psql postgres << 'EOF' +CREATE DATABASE nutrition_db; +CREATE USER nutrition_user WITH PASSWORD 'nutrition_password_2024'; +GRANT ALL PRIVILEGES ON DATABASE nutrition_db TO nutrition_user; +\q +EOF + +# Verificar conexión +psql -U nutrition_user -d nutrition_db -c "SELECT version();" +``` + +### Paso 4: Configurar variables de entorno + +```bash +# Crear archivo .env +cat > .env << 'EOF' +# Database +DATABASE_URL=postgresql://nutrition_user:nutrition_password_2024@localhost:5432/nutrition_db +REDIS_URL=redis://localhost:6379/0 + +# Security +SECRET_KEY=your-secret-key-change-this-in-production-use-openssl-rand-hex-32 +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# External APIs +OPENAI_API_KEY=sk-your-openai-api-key-here +USDA_API_KEY=your-usda-api-key-here +SPOONACULAR_API_KEY=your-spoonacular-api-key-here + +# Storage (Cloudflare R2) +CLOUDFLARE_R2_ACCESS_KEY=your-r2-access-key +CLOUDFLARE_R2_SECRET_KEY=your-r2-secret-key +CLOUDFLARE_R2_BUCKET=nutrition-photos +CLOUDFLARE_R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com + +# Environment +ENVIRONMENT=development +DEBUG=True +CORS_ORIGINS=http://localhost:3000,http://localhost:19006 +EOF + +# Generar SECRET_KEY segura +python -c "import secrets; print(f'SECRET_KEY={secrets.token_hex(32)}')" +# Copiar el output y reemplazar en .env +``` + +### Paso 5: Crear archivo de configuración + +```python +# backend/app/core/config.py +from pydantic_settings import BaseSettings +from typing import List + +class Settings(BaseSettings): + # Database + DATABASE_URL: str + REDIS_URL: str + + # Security + SECRET_KEY: str + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + REFRESH_TOKEN_EXPIRE_DAYS: int = 7 + + # External APIs + OPENAI_API_KEY: str + USDA_API_KEY: str + SPOONACULAR_API_KEY: str = "" + + # Storage + CLOUDFLARE_R2_ACCESS_KEY: str = "" + CLOUDFLARE_R2_SECRET_KEY: str = "" + CLOUDFLARE_R2_BUCKET: str = "" + CLOUDFLARE_R2_ENDPOINT: str = "" + + # App + ENVIRONMENT: str = "development" + DEBUG: bool = True + CORS_ORIGINS: str = "http://localhost:3000" + + @property + def cors_origins_list(self) -> List[str]: + return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] + + class Config: + env_file = ".env" + case_sensitive = True + +settings = Settings() +``` + +### Paso 6: Crear punto de entrada de FastAPI + +```python +# backend/app/main.py +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.core.config import settings + +app = FastAPI( + title="Nutrition Tracker API", + description="API para seguimiento nutricional con análisis de fotos", + version="1.0.0", + debug=settings.DEBUG +) + +# CORS +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins_list, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +@app.get("/") +async def root(): + return { + "message": "Nutrition Tracker API", + "version": "1.0.0", + "status": "running" + } + +@app.get("/health") +async def health_check(): + return {"status": "healthy"} + +# Aquí irán los routers de la API +# from app.api.v1 import auth, users, meals, etc. +# app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) +``` + +### Paso 7: Ejecutar servidor de desarrollo + +```bash +# Asegurar que estás en /backend con venv activado +cd backend +source venv/bin/activate # o venv\Scripts\activate en Windows + +# Ejecutar servidor +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 + +# Verificar en el navegador: +# http://localhost:8000 -> {"message": "Nutrition Tracker API"} +# http://localhost:8000/docs -> Documentación Swagger interactiva +``` + +--- + +## 4️⃣ Configurar Frontend (React + TypeScript) + +### Paso 1: Crear proyecto React + +```bash +# Volver a directorio raíz +cd .. + +# Crear app React con TypeScript +npx create-react-app frontend --template typescript + +cd frontend +``` + +### Paso 2: Instalar dependencias adicionales + +```bash +# Router +npm install react-router-dom +npm install -D @types/react-router-dom + +# Estado y queries +npm install zustand +npm install @tanstack/react-query + +# HTTP client +npm install axios + +# UI y estilos +npm install tailwindcss postcss autoprefixer +npx tailwindcss init -p + +# Utilidades +npm install date-fns +npm install clsx + +# Formularios +npm install react-hook-form +npm install zod + +# Gráficos +npm install recharts + +# Iconos +npm install lucide-react +``` + +### Paso 3: Configurar Tailwind CSS + +```javascript +// frontend/tailwind.config.js +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "./src/**/*.{js,jsx,ts,tsx}", + ], + theme: { + extend: { + colors: { + primary: { + 50: '#f0fdf4', + 100: '#dcfce7', + 500: '#22c55e', + 600: '#16a34a', + 700: '#15803d', + }, + }, + }, + }, + plugins: [], +} +``` + +```css +/* frontend/src/index.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +### Paso 4: Configurar variables de entorno + +```bash +# Crear .env +cat > .env << 'EOF' +REACT_APP_API_URL=http://localhost:8000/api/v1 +REACT_APP_ENVIRONMENT=development +EOF +``` + +### Paso 5: Crear estructura base + +```typescript +// frontend/src/services/api.ts +import axios from 'axios'; + +const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/v1'; + +export const api = axios.create({ + baseURL: API_URL, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Interceptor para JWT +api.interceptors.request.use((config) => { + const token = localStorage.getItem('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +export default api; +``` + +### Paso 6: Ejecutar frontend + +```bash +# En directorio /frontend +npm start + +# Se abrirá automáticamente en http://localhost:3000 +``` + +--- + +## 5️⃣ Configurar Migraciones de Base de Datos + +```bash +cd backend + +# Inicializar Alembic +alembic init alembic + +# Editar alembic.ini - cambiar sqlalchemy.url +# sqlalchemy.url = postgresql://nutrition_user:nutrition_password_2024@localhost:5432/nutrition_db + +# O mejor, usar variable de entorno en alembic/env.py: +``` + +```python +# backend/alembic/env.py +from logging.config import fileConfig +from sqlalchemy import engine_from_config, pool +from alembic import context +import os +from dotenv import load_dotenv + +# Cargar variables de entorno +load_dotenv() + +# Importar base de modelos +from app.db.base import Base +from app.models.user import User # Importar todos los modelos +# from app.models.meal import Meal +# etc... + +# this is the Alembic Config object +config = context.config + +# Usar DATABASE_URL del .env +config.set_main_option('sqlalchemy.url', os.getenv('DATABASE_URL')) + +# Interpret the config file for Python logging +fileConfig(config.config_file_name) + +target_metadata = Base.metadata + +# ... resto del archivo generado por Alembic +``` + +```bash +# Crear primera migración +alembic revision --autogenerate -m "Create initial tables" + +# Ejecutar migraciones +alembic upgrade head + +# Verificar tablas creadas +psql -U nutrition_user -d nutrition_db -c "\dt" +``` + +--- + +## 6️⃣ Obtener API Keys + +### USDA FoodData Central (GRATIS) + +1. Ir a https://fdc.nal.usda.gov/api-guide.html +2. Click en "Get an API Key" +3. Completar formulario (nombre, email, uso: "Nutrition tracking app") +4. Copiar API key y agregar a `.env`: + ``` + USDA_API_KEY=tu-api-key-aqui + ``` + +### OpenAI GPT-4 Vision (PAGO) + +1. Ir a https://platform.openai.com/signup +2. Crear cuenta y agregar método de pago +3. Ir a https://platform.openai.com/api-keys +4. Click en "Create new secret key" +5. Copiar y agregar a `.env`: + ``` + OPENAI_API_KEY=sk-tu-api-key-aqui + ``` +6. **Importante:** Configurar límite de gasto mensual en Settings > Billing + +### Spoonacular API (Opcional - 150 requests/día gratis) + +1. Ir a https://spoonacular.com/food-api +2. Click en "Get Started" y crear cuenta +3. En Dashboard, copiar API Key +4. Agregar a `.env`: + ``` + SPOONACULAR_API_KEY=tu-api-key-aqui + ``` + +--- + +## 7️⃣ Probar Configuración Completa + +### Script de verificación + +```python +# backend/scripts/verify_setup.py +import os +import sys +from dotenv import load_dotenv +import httpx +from sqlalchemy import create_engine, text + +load_dotenv() + +def check_env_vars(): + """Verificar variables de entorno""" + required_vars = [ + 'DATABASE_URL', + 'SECRET_KEY', + 'OPENAI_API_KEY', + 'USDA_API_KEY' + ] + + missing = [] + for var in required_vars: + if not os.getenv(var): + missing.append(var) + + if missing: + print(f"❌ Faltan variables: {', '.join(missing)}") + return False + + print("✅ Variables de entorno configuradas") + return True + +def check_database(): + """Verificar conexión a base de datos""" + try: + engine = create_engine(os.getenv('DATABASE_URL')) + with engine.connect() as conn: + result = conn.execute(text("SELECT 1")) + print("✅ Conexión a PostgreSQL exitosa") + return True + except Exception as e: + print(f"❌ Error de base de datos: {e}") + return False + +async def check_usda_api(): + """Verificar USDA API""" + try: + async with httpx.AsyncClient() as client: + response = await client.get( + "https://api.nal.usda.gov/fdc/v1/foods/search", + params={ + "api_key": os.getenv('USDA_API_KEY'), + "query": "apple", + "pageSize": 1 + } + ) + response.raise_for_status() + print("✅ USDA API funcionando") + return True + except Exception as e: + print(f"❌ Error USDA API: {e}") + return False + +if __name__ == "__main__": + print("\n🔍 Verificando configuración...\n") + + checks = [ + check_env_vars(), + check_database(), + ] + + # USDA API check (async) + import asyncio + checks.append(asyncio.run(check_usda_api())) + + print("\n" + "="*50) + if all(checks): + print("🎉 ¡Configuración completa y funcionando!") + sys.exit(0) + else: + print("⚠️ Hay problemas con la configuración") + sys.exit(1) +``` + +```bash +# Ejecutar verificación +cd backend +python scripts/verify_setup.py +``` + +--- + +## 8️⃣ Comandos Útiles de Desarrollo + +### Backend + +```bash +# Activar entorno virtual +cd backend +source venv/bin/activate # Linux/Mac +venv\Scripts\activate # Windows + +# Ejecutar servidor de desarrollo +uvicorn app.main:app --reload + +# Ejecutar tests +pytest + +# Generar migración +alembic revision --autogenerate -m "descripcion" + +# Aplicar migraciones +alembic upgrade head + +# Rollback última migración +alembic downgrade -1 + +# Ver documentación interactiva +# http://localhost:8000/docs +``` + +### Frontend + +```bash +cd frontend + +# Ejecutar en desarrollo +npm start + +# Build para producción +npm run build + +# Ejecutar tests +npm test + +# Linter +npm run lint +``` + +### Docker (Opcional - Setup completo) + +```yaml +# docker-compose.yml (en raíz del proyecto) +version: '3.8' + +services: + db: + image: postgres:14 + environment: + POSTGRES_USER: nutrition_user + POSTGRES_PASSWORD: nutrition_password_2024 + POSTGRES_DB: nutrition_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + ports: + - "6379:6379" + + backend: + build: ./backend + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + volumes: + - ./backend:/app + ports: + - "8000:8000" + depends_on: + - db + - redis + env_file: + - ./backend/.env + + frontend: + build: ./frontend + command: npm start + volumes: + - ./frontend:/app + - /app/node_modules + ports: + - "3000:3000" + depends_on: + - backend + +volumes: + postgres_data: +``` + +```bash +# Levantar todos los servicios +docker-compose up -d + +# Ver logs +docker-compose logs -f + +# Detener servicios +docker-compose down +``` + +--- + +## 9️⃣ Próximos Pasos + +Una vez completada la configuración: + +1. ✅ **Implementar modelos de base de datos** + - Crear modelos en `backend/app/models/` + - Generar y aplicar migraciones + +2. ✅ **Desarrollar endpoints de autenticación** + - Registro de usuarios + - Login/Logout + - Refresh token + +3. ✅ **Implementar cálculo nutricional** + - Servicio de cálculo de TMB/TDEE + - API endpoint para calcular perfil nutricional + +4. ✅ **Integrar análisis de fotos** + - Servicio de GPT-4 Vision + - Endpoint de upload y análisis + +5. ✅ **Desarrollar frontend básico** + - Pantalla de login/registro + - Dashboard principal + - Formulario de agregar comida + +--- + +## 🐛 Solución de Problemas Comunes + +### Error: `ModuleNotFoundError: No module named 'app'` + +```bash +# Asegurar que estás en el directorio correcto +cd backend + +# Verificar que app/ tenga __init__.py +touch app/__init__.py + +# Ejecutar desde el directorio correcto +uvicorn app.main:app --reload +``` + +### Error: `psycopg2.OperationalError: connection refused` + +```bash +# Verificar que PostgreSQL está corriendo +sudo service postgresql status # Linux +brew services list # macOS + +# Iniciar PostgreSQL +sudo service postgresql start # Linux +brew services start postgresql # macOS + +# Verificar credenciales en .env +``` + +### Error: `CORS policy` en frontend + +```python +# backend/app/main.py - Verificar configuración de CORS +app.add_middleware( + CORSMiddleware, + allow_origins=["http://localhost:3000"], # URL del frontend + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +``` + +### Frontend: `Module not found: Can't resolve 'XXX'` + +```bash +# Reinstalar node_modules +cd frontend +rm -rf node_modules package-lock.json +npm install +``` + +--- + +## 📚 Recursos Adicionales + +- **FastAPI Docs**: https://fastapi.tiangolo.com/ +- **React Docs**: https://react.dev/ +- **SQLAlchemy**: https://docs.sqlalchemy.org/ +- **Alembic**: https://alembic.sqlalchemy.org/ +- **Tailwind CSS**: https://tailwindcss.com/docs +- **Recharts**: https://recharts.org/ + +--- + +**¡Configuración completa! 🎉** + +Ahora estás listo para empezar a desarrollar la aplicación de nutrición. + +**Documento creado**: 2026-01-02 +**Versión**: 1.0