const db = require('../models'); const { Op } = require('sequelize'); function calculateBMR(weightKg, heightCm, age, gender) { // Calculate Basal Metabolic Rate using Mifflin-St Jeor Equation let bmr; if (gender.toLowerCase() === 'male') { bmr = (10 * weightKg) + (6.25 * heightCm) - (5 * age) + 5; } else { // female bmr = (10 * weightKg) + (6.25 * heightCm) - (5 * age) - 161; } return Math.round(bmr); } function calculateTDEE(bmr, activityLevel) { // Calculate Total Daily Energy Expenditure const multipliers = { 'sedentary': 1.2, 'light': 1.375, 'moderate': 1.55, 'active': 1.725, 'very_active': 1.9 }; const multiplier = multipliers[activityLevel] || 1.55; return Math.round(bmr * multiplier); } function calculateMacroTargets(weightKg, goalType = 'recomp') { // Calculate macro targets based on body weight and goal let protein, carbs, fat; if (goalType === 'muscle_gain') { protein = weightKg * 2.4; // High protein for muscle building carbs = weightKg * 3.5; // Higher carbs for energy fat = weightKg * 1.0; // Moderate fat } else if (goalType === 'weight_loss') { protein = weightKg * 2.2; // High protein to preserve muscle carbs = weightKg * 2.0; // Lower carbs for deficit fat = weightKg * 0.8; // Lower fat } else { // recomp (body recomposition) protein = weightKg * 2.2; // High protein carbs = weightKg * 2.5; // Moderate carbs fat = weightKg * 0.9; // Moderate fat } return { protein_g: Math.round(protein), carbs_g: Math.round(carbs), fat_g: Math.round(fat) }; } async function calculateDailyTotals(userId, targetDate = null) { // Calculate total nutrition consumed for a given date if (!targetDate) { targetDate = new Date(); } // Ensure date is in YYYY-MM-DD format if it's a string, or create date object const dateStr = targetDate instanceof Date ? targetDate.toISOString().split('T')[0] : targetDate; // Get all meals for the date const meals = await db.Meal.findAll({ where: { UserId: userId, date: dateStr }, include: [{ model: db.MealFood, include: [db.FoodItem] }] }); const totals = { calories: 0, protein: 0, carbs: 0, fat: 0, meals: [] }; for (const meal of meals) { const mealTotals = { calories: 0, protein: 0, carbs: 0, fat: 0 }; const mealFoods = []; for (const mf of meal.MealFoods) { // Calculate nutrition for this food item based on quantity // If calculated values exist in MealFood, use them, otherwise calculate const quantity = mf.quantity || 1.0; const food = mf.FoodItem; const calories = mf.calories_consumed || (food.calories * quantity); const protein = mf.protein_consumed || (food.protein_g * quantity); const carbs = mf.carbs_consumed || (food.carbs_g * quantity); const fat = mf.fat_consumed || (food.fat_g * quantity); mealTotals.calories += calories; mealTotals.protein += protein; mealTotals.carbs += carbs; mealTotals.fat += fat; mealFoods.push({ name: food.name, quantity: quantity, calories: calories }); } totals.calories += mealTotals.calories; totals.protein += mealTotals.protein; totals.carbs += mealTotals.carbs; totals.fat += mealTotals.fat; totals.meals.push({ id: meal.id, type: meal.meal_type, time: meal.time ? meal.time.substring(0, 5) : null, totals: mealTotals, foods: mealFoods }); } return totals; } async function calculateWaterTotal(userId, targetDate = null) { // Calculate total water intake for a given date if (!targetDate) { targetDate = new Date(); } const dateStr = targetDate instanceof Date ? targetDate.toISOString().split('T')[0] : targetDate; const waterLogs = await db.WaterLog.findAll({ where: { UserId: userId, date: dateStr } }); const total = waterLogs.reduce((sum, log) => sum + log.amount_ml, 0); return { total_ml: total, logs: waterLogs.map(log => ({ id: log.id, amount_ml: log.amount_ml, time: log.time ? log.time.substring(0, 5) : null })) }; } async function getWeightTrend(userId, days = 7) { // Get weight trend for the past N days const endDate = new Date(); const startDate = new Date(); startDate.setDate(endDate.getDate() - (days - 1)); const startDateStr = startDate.toISOString().split('T')[0]; const endDateStr = endDate.toISOString().split('T')[0]; const weightLogs = await db.WeightLog.findAll({ where: { UserId: userId, date: { [Op.between]: [startDateStr, endDateStr] } }, order: [['date', 'ASC']] }); return weightLogs.map(log => ({ date: log.date, weight_kg: log.weight_kg })); } async function getCalorieTrend(userId, days = 7) { // Get calorie intake trend for the past N days const endDate = new Date(); const startDate = new Date(); startDate.setDate(endDate.getDate() - (days - 1)); const trend = []; let currentDate = new Date(startDate); while (currentDate <= endDate) { const dateStr = currentDate.toISOString().split('T')[0]; const totals = await calculateDailyTotals(userId, dateStr); trend.push({ date: dateStr, calories: Math.round(totals.calories), protein: Math.round(totals.protein), carbs: Math.round(totals.carbs), fat: Math.round(totals.fat) }); currentDate.setDate(currentDate.getDate() + 1); } return trend; } async function updateDailySummary(userId, targetDate = null) { // Update or create daily summary for a user if (!targetDate) { targetDate = new Date(); } const dateStr = targetDate instanceof Date ? targetDate.toISOString().split('T')[0] : targetDate; // Calculate totals const nutrition = await calculateDailyTotals(userId, dateStr); const water = await calculateWaterTotal(userId, dateStr); // Get weight for the day const weightLog = await db.WeightLog.findOne({ where: { UserId: userId, date: dateStr } }); const weight = weightLog ? weightLog.weight_kg : null; // Get user's calorie target const user = await db.User.findByPk(userId); const targetCalories = user ? user.target_daily_calories : 2000; // Find or create summary let summary = await db.DailySummary.findOne({ where: { UserId: userId, date: dateStr } }); if (!summary) { summary = await db.DailySummary.create({ UserId: userId, date: dateStr }); } // Update values summary.total_calories = nutrition.calories; summary.total_protein_g = nutrition.protein; summary.total_carbs_g = nutrition.carbs; summary.total_fat_g = nutrition.fat; summary.total_water_ml = water.total_ml; summary.calories_remaining = targetCalories - nutrition.calories; summary.weight_kg = weight; try { await summary.save(); return summary; } catch (e) { console.error(`Error updating daily summary: ${e}`); return null; } } function getMacroPercentages(proteinG, carbsG, fatG) { // Calculate macro distribution as percentages const proteinCal = proteinG * 4; const carbsCal = carbsG * 4; const fatCal = fatG * 9; const totalCal = proteinCal + carbsCal + fatCal; if (totalCal === 0) { return { protein: 0, carbs: 0, fat: 0 }; } return { protein: Math.round((proteinCal / totalCal) * 100), carbs: Math.round((carbsCal / totalCal) * 100), fat: Math.round((fatCal / totalCal) * 100) }; } function suggestFoodsForMacros(remainingProtein, remainingCarbs, remainingFat) { // Suggest Filipino foods based on remaining macros const suggestions = []; // High protein needed if (remainingProtein > 30) { suggestions.push({ category: 'High Protein Ulam', examples: ['Grilled Tilapia', 'Chicken Tinola', 'Grilled Chicken'] }); } // High carbs needed if (remainingCarbs > 40) { suggestions.push({ category: 'Carbs', examples: ['White Rice', 'Pandesal', 'Sweet Potato'] }); } // High fat needed if (remainingFat > 20) { suggestions.push({ category: 'Healthy Fats', examples: ['Sisig', 'Lechon Kawali', 'Bicol Express'] }); } // Balanced meal needed if (remainingProtein > 20 && remainingCarbs > 30) { suggestions.push({ category: 'Balanced Meals', examples: ['Tapsilog', 'Chicken Adobo with Rice', 'Sinigang'] }); } return suggestions; } async function generateDailyMealPlan(userId, dateStr) { try { // Get user's calorie target const user = await db.User.findByPk(userId); const targetCalories = user ? user.target_daily_calories : 2000; // Distribution: Breakfast 25%, Lunch 35%, Dinner 30%, Snack 10% const targets = { breakfast: Math.round(targetCalories * 0.25), lunch: Math.round(targetCalories * 0.35), dinner: Math.round(targetCalories * 0.30), snack: Math.round(targetCalories * 0.10) }; // Fetch all foods const foods = await db.FoodItem.findAll(); // Categorize foods const foodByCat = { almusal: foods.filter(f => f.category === 'almusal'), ulam: foods.filter(f => ['ulam', 'sabaw'].includes(f.category)), kanin: foods.filter(f => f.category === 'kanin'), gulay: foods.filter(f => f.category === 'gulay'), meryenda: foods.filter(f => f.category === 'meryenda') }; // Fallback if categories are empty (use all foods) const allFoods = foods; // Helper to pick random item const pickRandom = (arr) => arr.length > 0 ? arr[Math.floor(Math.random() * arr.length)] : null; // Helper to generate a meal const generateMeal = async (mealType, targetCal) => { let currentCal = 0; const selectedFoods = []; // Strategy per meal type if (mealType === 'breakfast') { // Try almusal first const main = pickRandom(foodByCat.almusal) || pickRandom(allFoods); if (main) { selectedFoods.push({ food: main, qty: 1 }); currentCal += main.calories; } } else if (['lunch', 'dinner'].includes(mealType)) { // Rice + Ulam + (maybe) Gulay const rice = pickRandom(foodByCat.kanin); const ulam = pickRandom(foodByCat.ulam) || pickRandom(allFoods); const gulay = pickRandom(foodByCat.gulay); if (rice) { selectedFoods.push({ food: rice, qty: 1 }); currentCal += rice.calories; } if (ulam) { selectedFoods.push({ food: ulam, qty: 1 }); currentCal += ulam.calories; } // Add vegetable if we have room if (gulay && currentCal < targetCal) { selectedFoods.push({ food: gulay, qty: 1 }); currentCal += gulay.calories; } } else if (mealType === 'snack') { const snack = pickRandom(foodByCat.meryenda) || pickRandom(allFoods); if (snack) { selectedFoods.push({ food: snack, qty: 1 }); currentCal += snack.calories; } } // Adjust quantities to closer match target (simple scaling) if (currentCal > 0 && selectedFoods.length > 0) { const ratio = targetCal / currentCal; // Limit scaling to reasonable bounds (0.5x to 2.0x) to avoid weird portions // But for simplicity, let's just create the plan as is, maybe adding another item if way under // Create MealPlan const mealPlan = await db.MealPlan.create({ UserId: userId, date: dateStr, meal_type: mealType, is_completed: false }); // Add foods for (const item of selectedFoods) { await db.PlannedFood.create({ MealPlanId: mealPlan.id, FoodItemId: item.food.id, quantity: item.qty }); } } }; // Delete existing plans for this date? Or just append? // Let's clear existing plans for this date to avoid duplicates if re-generating await db.MealPlan.destroy({ where: { UserId: userId, date: dateStr } }); // Generate for each meal type await generateMeal('breakfast', targets.breakfast); await generateMeal('lunch', targets.lunch); await generateMeal('dinner', targets.dinner); await generateMeal('snack', targets.snack); return true; } catch (error) { console.error('Error generating meal plan:', error); throw error; } } module.exports = { calculateBMR, calculateTDEE, calculateMacroTargets, calculateDailyTotals, calculateWaterTotal, getWeightTrend, getCalorieTrend, updateDailySummary, getMacroPercentages, suggestFoodsForMacros, generateDailyMealPlan };