From 0fa03437981d962d0992a286e3666715ec086245 Mon Sep 17 00:00:00 2001 From: Jp Date: Sat, 31 Jan 2026 00:56:25 +0800 Subject: [PATCH] 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. --- .devdbrc | 6 ++ data/calorie_tracker.db | Bin 77824 -> 77824 bytes data/sessions.db | Bin 12288 -> 12288 bytes scripts/seed.js | 116 ++++++++++++++++++++++++++++++++++ server.js | 52 +++++++++++----- utils/index.js | 134 +++++++++++++++++++++++++++++++++++++--- views/meal_planner.ejs | 19 ++++-- 7 files changed, 300 insertions(+), 27 deletions(-) create mode 100644 .devdbrc diff --git a/.devdbrc b/.devdbrc new file mode 100644 index 0000000..dc44e22 --- /dev/null +++ b/.devdbrc @@ -0,0 +1,6 @@ +[ + { + "type": "sqlite", + "path": "d:\\projects\\calorie_tracker\\calorie_tracker_app\\data\\calorie_tracker.db" + } +] \ No newline at end of file diff --git a/data/calorie_tracker.db b/data/calorie_tracker.db index e2825ac11f443b5a514ddc7b98e531e68c3ef3b9..29fc8cffd8a3ed778117c7c56eff4ccebdefff1b 100644 GIT binary patch delta 1563 zcmaKr&u`LT9LC=l*l2;aOks>nq-4<;9kb;}`Ei+ME0b&*G+W$hp@Vmftb>J)!*1h0 znCZcrnOS1uMdL6rOFWsFEb-#Sn}_XSyt<<&ZJ}?=5V_Fz3p~&Bd7t;~{+VO{%yBRY z#b?*%p!lx%@-D-i0ta&fm<#47bHaRUkFl?r1dP7`=e!R4 z4Ty(&y{s^V%^o0E6x2{CsOfUrm-$_^twfIRISL*JeFT_SOo9GQAGOE4D$OqwB!~nE zkk6AbE+#}dJ{sqgnpE1dMwS<{iF7uenGsSk^&)q5u4iWNa9oz-R-Bqou+OJl)hHVS zjRf_4(P#Mx3BzZ3bO4Xstd%wg+9WxhXTju2a)!_9h~^XS<5_PYX=_7Lb_aSTI(xfq zdL?3p7o~OXq`g2p6NQX?!##psbNAPr59-lg_M!#w27Xf!oEZSCyr90dzdgO zTuw|1M&hV@-qjWMT*;n0n2hVfxLHmzj)fvQEHKg8qR-SN@D@kB#|6l?;a7WMoOA^N zvqrz69aPct+2eDsxjwpr+DZQ*XIpH<4#%7^EVE^4y`cj2jMV|wBg7Hl3b7K zALarF!cU$R0v_;eMV1srZQb{|E!)mnxvW|JmGdEM8Oc;*Hj86{RZI zD@%AG$=gjul5MNL=oDPz*?Oal8tkn`wS=ZwRH;m}ilqIag4D;4{3ZyW=^SDIT*M^W z@0eufx*vo>CdsTpvMlX%K=2cD+?zCPX@eAtpjPL;%+j vFd*hO>pMyl?cryoDNB3cy~pJOr|R2Wu*dpx@qAh@{)&}^i~mrJ!b96%e3)yz delta 100 zcmZp8z|!!5Wr8%L#zYxsMvaXL3-!4f7#NuNpEK}(^aYEdyS zBeOiCTYi3uXGv;qvCJk1mVnJH0e|f`GhSd0;9=oSVBnAATgL}vY!(y<;N6~}%ea6E E0KL#0DgXcg diff --git a/data/sessions.db b/data/sessions.db index 9e11b4e79cd13910389d6d3cd60be9f90620b881..fa797e9b73ecfccf52c1b03b687230fadab7bec0 100644 GIT binary patch delta 57 zcmZojXh@hK&FDB$#+lJ^W5RwpMuo`=@|ui_lfC3MSe80kT$o%YZ_jOJWol++V5Dbm NV7U2!yuARUA^_-X57Ynv delta 57 zcmZojXh@hK%_uri#+gxcW5RwpMz+Zc@|uk7lfC3MSQe;#FPK~=Z_jONWn^Y$Y^Y~x NY`OV>yuAP;I{?>L4|o6o diff --git a/scripts/seed.js b/scripts/seed.js index fa1430b..07a677e 100644 --- a/scripts/seed.js +++ b/scripts/seed.js @@ -319,6 +319,122 @@ const filipinoFoods = [ fat_g: 12, serving_description: '1 piece', serving_size_g: 120 + }, + + // Vegetables (Gulay) + { + name: 'Pinakbet', + name_tagalog: 'Pinakbet', + category: 'gulay', + calories: 150, + protein_g: 6, + carbs_g: 15, + fat_g: 8, + serving_description: '1 cup', + serving_size_g: 200 + }, + { + name: 'Ginisang Monggo', + name_tagalog: 'Ginisang Monggo', + category: 'gulay', + calories: 220, + protein_g: 15, + carbs_g: 30, + fat_g: 6, + serving_description: '1 cup', + serving_size_g: 240 + }, + { + name: 'Chopsuey', + name_tagalog: 'Chopsuey', + category: 'gulay', + calories: 180, + protein_g: 12, + carbs_g: 10, + fat_g: 10, + serving_description: '1 cup', + serving_size_g: 200 + }, + { + name: 'Laing', + name_tagalog: 'Laing', + category: 'gulay', + calories: 250, + protein_g: 5, + carbs_g: 12, + fat_g: 22, + serving_description: '1 serving', + serving_size_g: 150 + }, + + // Breakfast + { + name: 'Tapsilog', + name_tagalog: 'Tapsilog', + category: 'almusal', + calories: 550, + protein_g: 35, + carbs_g: 45, + fat_g: 25, + serving_description: '1 order (rice, egg, beef)', + serving_size_g: 350 + }, + { + name: 'Longsilog', + name_tagalog: 'Longsilog', + category: 'almusal', + calories: 600, + protein_g: 20, + carbs_g: 45, + fat_g: 35, + serving_description: '1 order (rice, egg, sausage)', + serving_size_g: 350 + }, + { + name: 'Pandesal', + name_tagalog: 'Pandesal', + category: 'almusal', + calories: 35, + protein_g: 1.5, + carbs_g: 6, + fat_g: 0.5, + serving_description: '1 piece', + serving_size_g: 30 + }, + + // Desserts/Snacks + { + name: 'Halo-Halo', + name_tagalog: 'Halo-Halo', + category: 'meryenda', + calories: 450, + protein_g: 12, + carbs_g: 85, + fat_g: 8, + serving_description: '1 glass', + serving_size_g: 400 + }, + { + name: 'Leche Flan', + name_tagalog: 'Leche Flan', + category: 'meryenda', + calories: 220, + protein_g: 5, + carbs_g: 25, + fat_g: 10, + serving_description: '1 slice', + serving_size_g: 80 + }, + { + name: 'Turon', + name_tagalog: 'Turon', + category: 'meryenda', + calories: 200, + protein_g: 2, + carbs_g: 35, + fat_g: 7, + serving_description: '1 piece', + serving_size_g: 100 } ]; diff --git a/server.js b/server.js index 5ee31e0..e4b478e 100644 --- a/server.js +++ b/server.js @@ -153,10 +153,10 @@ app.get('/dashboard', ensureAuthenticated, async (req, res) => { const water = await utils.calculateWaterTotal(req.user.id, dateStr); // Get user goals - let goals = await db.UserGoal.findOne({ where: { user_id: req.user.id } }); + let goals = await db.UserGoal.findOne({ where: { UserId: req.user.id } }); if (!goals) { goals = await db.UserGoal.create({ - user_id: req.user.id, + UserId: req.user.id, target_protein_g: 150, target_carbs_g: 200, target_fat_g: 60, @@ -166,7 +166,7 @@ app.get('/dashboard', ensureAuthenticated, async (req, res) => { // Get weight info const weightLogToday = await db.WeightLog.findOne({ - where: { user_id: req.user.id, date: dateStr } + where: { UserId: req.user.id, date: dateStr } }); const yesterday = new Date(today); @@ -174,7 +174,7 @@ app.get('/dashboard', ensureAuthenticated, async (req, res) => { const yesterdayStr = yesterday.toISOString().split('T')[0]; const weightLogYesterday = await db.WeightLog.findOne({ - where: { user_id: req.user.id, date: yesterdayStr } + where: { UserId: req.user.id, date: yesterdayStr } }); let weightChange = null; @@ -259,7 +259,7 @@ app.post('/add-meal', ensureAuthenticated, async (req, res) => { } const meal = await db.Meal.create({ - user_id: req.user.id, + UserId: req.user.id, date: date || new Date(), meal_type, time: time || null @@ -337,7 +337,7 @@ app.post('/add-water', ensureAuthenticated, async (req, res) => { const date = req.body.date || new Date().toISOString().split('T')[0]; await db.WaterLog.create({ - user_id: req.user.id, + UserId: req.user.id, date: date, amount_ml: amountMl, time: new Date() @@ -360,7 +360,7 @@ app.post('/add-weight', ensureAuthenticated, async (req, res) => { const date = req.body.date || new Date().toISOString().split('T')[0]; let weightLog = await db.WeightLog.findOne({ - where: { user_id: req.user.id, date: date } + where: { UserId: req.user.id, date: date } }); if (weightLog) { @@ -369,7 +369,7 @@ app.post('/add-weight', ensureAuthenticated, async (req, res) => { await weightLog.save(); } else { await db.WeightLog.create({ - user_id: req.user.id, + UserId: req.user.id, date: date, weight_kg: weightKg, time: new Date() @@ -388,7 +388,7 @@ app.post('/add-weight', ensureAuthenticated, async (req, res) => { app.get('/goals', ensureAuthenticated, async (req, res) => { try { - const userGoals = await db.UserGoal.findOne({ where: { user_id: req.user.id } }); + const userGoals = await db.UserGoal.findOne({ where: { UserId: req.user.id } }); let bmr = null; let tdee = null; @@ -444,9 +444,9 @@ app.post('/goals', ensureAuthenticated, async (req, res) => { await user.save(); // Update or create goals - let userGoals = await db.UserGoal.findOne({ where: { user_id: req.user.id } }); + let userGoals = await db.UserGoal.findOne({ where: { UserId: req.user.id } }); if (!userGoals) { - userGoals = await db.UserGoal.create({ user_id: req.user.id }); + userGoals = await db.UserGoal.create({ UserId: req.user.id }); } userGoals.goal_type = goalType; @@ -578,7 +578,7 @@ app.get('/meal-planner', ensureAuthenticated, async (req, res) => { const dateStr = d.toISOString().split('T')[0]; const plans = await db.MealPlan.findAll({ - where: { user_id: req.user.id, date: dateStr }, + where: { UserId: req.user.id, date: dateStr }, include: [{ model: db.PlannedFood, include: [db.FoodItem] @@ -630,6 +630,26 @@ app.get('/meal-planner', ensureAuthenticated, async (req, res) => { } }); +app.post('/meal-planner/auto-generate', ensureAuthenticated, async (req, res) => { + try { + const { date } = req.body; + + if (!date) { + req.flash('error_msg', 'Please select a date'); + return res.redirect('/meal-planner'); + } + + await utils.generateDailyMealPlan(req.user.id, date); + + req.flash('success_msg', 'Meal plan generated successfully!'); + res.redirect('/meal-planner'); + } catch (err) { + console.error(err); + req.flash('error_msg', 'Error generating meal plan'); + res.redirect('/meal-planner'); + } +}); + app.post('/meal-planner/add', ensureAuthenticated, async (req, res) => { try { let { date, meal_type, 'food_id[]': foodIds, 'quantity[]': quantities } = req.body; @@ -648,7 +668,7 @@ app.post('/meal-planner/add', ensureAuthenticated, async (req, res) => { // Find or create meal plan let mealPlan = await db.MealPlan.findOne({ where: { - user_id: req.user.id, + UserId: req.user.id, date: date, meal_type: meal_type } @@ -656,7 +676,7 @@ app.post('/meal-planner/add', ensureAuthenticated, async (req, res) => { if (!mealPlan) { mealPlan = await db.MealPlan.create({ - user_id: req.user.id, + UserId: req.user.id, date: date, meal_type: meal_type }); @@ -688,7 +708,7 @@ app.post('/meal-planner/add', ensureAuthenticated, async (req, res) => { app.post('/meal-planner/complete/:id', ensureAuthenticated, async (req, res) => { try { const plan = await db.MealPlan.findOne({ - where: { id: req.params.id, user_id: req.user.id } + where: { id: req.params.id, UserId: req.user.id } }); if (plan) { @@ -710,7 +730,7 @@ app.post('/meal-planner/complete/:id', ensureAuthenticated, async (req, res) => app.post('/meal-planner/delete/:id', ensureAuthenticated, async (req, res) => { try { await db.MealPlan.destroy({ - where: { id: req.params.id, user_id: req.user.id } + where: { id: req.params.id, UserId: req.user.id } }); req.flash('success_msg', 'Meal plan deleted'); res.redirect('/meal-planner'); diff --git a/utils/index.js b/utils/index.js index c6551d5..e8c1ce6 100644 --- a/utils/index.js +++ b/utils/index.js @@ -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 }; diff --git a/views/meal_planner.ejs b/views/meal_planner.ejs index 1242961..126bd47 100644 --- a/views/meal_planner.ejs +++ b/views/meal_planner.ejs @@ -64,10 +64,21 @@ <% } %> - - + +
+ + + + +
+ + +
+
<% }); %>