feat(meal-planner): add auto-generate meal plan functionality

Add new utility function to generate daily meal plans based on user's calorie targets and food categories. Include auto-generate button in meal planner UI with confirmation prompt. Add new Filipino food items to seed data for better meal variety. Fix database column references to use capitalized field names consistently.
This commit is contained in:
Jp
2026-01-31 00:56:25 +08:00
parent cb2fe12c73
commit 0fa0343798
7 changed files with 300 additions and 27 deletions

View File

@@ -65,7 +65,7 @@ async function calculateDailyTotals(userId, targetDate = null) {
// Get all meals for the date
const meals = await db.Meal.findAll({
where: {
user_id: userId,
UserId: userId,
date: dateStr
},
include: [{
@@ -142,7 +142,7 @@ async function calculateWaterTotal(userId, targetDate = null) {
const waterLogs = await db.WaterLog.findAll({
where: {
user_id: userId,
UserId: userId,
date: dateStr
}
});
@@ -170,7 +170,7 @@ async function getWeightTrend(userId, days = 7) {
const weightLogs = await db.WeightLog.findAll({
where: {
user_id: userId,
UserId: userId,
date: {
[Op.between]: [startDateStr, endDateStr]
}
@@ -226,7 +226,7 @@ async function updateDailySummary(userId, targetDate = null) {
// Get weight for the day
const weightLog = await db.WeightLog.findOne({
where: {
user_id: userId,
UserId: userId,
date: dateStr
}
});
@@ -239,14 +239,14 @@ async function updateDailySummary(userId, targetDate = null) {
// Find or create summary
let summary = await db.DailySummary.findOne({
where: {
user_id: userId,
UserId: userId,
date: dateStr
}
});
if (!summary) {
summary = await db.DailySummary.create({
user_id: userId,
UserId: userId,
date: dateStr
});
}
@@ -326,6 +326,125 @@ function suggestFoodsForMacros(remainingProtein, remainingCarbs, remainingFat) {
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,
@@ -336,5 +455,6 @@ module.exports = {
getCalorieTrend,
updateDailySummary,
getMacroPercentages,
suggestFoodsForMacros
suggestFoodsForMacros,
generateDailyMealPlan
};