Files
calorie_tracker_2/server.js
Jp 0fa0343798 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.
2026-01-31 00:56:25 +08:00

755 lines
21 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const expressLayouts = require('express-ejs-layouts');
const flash = require('express-flash');
const methodOverride = require('method-override');
const path = require('path');
const SQLiteStore = require('connect-sqlite3')(session);
const db = require('./models');
const utils = require('./utils');
const { NutritionAPI, searchAllSources } = require('./utils/api_client');
const app = express();
const PORT = process.env.PORT || 5001;
// Initialize API Client
const apiClient = new NutritionAPI(process.env.API_NINJAS_KEY);
// Passport Config
passport.use(new LocalStrategy(
async (username, password, done) => {
try {
const user = await db.User.findOne({ where: { username } });
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
if (!user.validPassword(password)) {
return done(null, false, { message: 'Incorrect password.' });
}
return done(null, user);
} catch (err) {
return done(err);
}
}
));
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await db.User.findByPk(id);
done(null, user);
} catch (err) {
done(err);
}
});
// Middleware
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.set('layout', 'layout');
app.use(expressLayouts);
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(methodOverride('_method'));
// Session
app.use(session({
store: new SQLiteStore({ db: 'sessions.db', dir: './data' }),
secret: process.env.SECRET_KEY || 'secret',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } // 30 days
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(flash());
// Global Variables
app.use((req, res, next) => {
res.locals.current_user = req.user;
res.locals.success_msg = req.flash('success_msg');
res.locals.error_msg = req.flash('error_msg');
res.locals.error = req.flash('error');
res.locals.path = req.path;
next();
});
// Helper functions for templates
app.locals.round = Math.round;
// Auth Middleware
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
req.flash('error_msg', 'Please log in to view that resource');
res.redirect('/login');
}
// Routes
app.get('/', (req, res) => {
if (req.isAuthenticated()) {
res.redirect('/dashboard');
} else {
res.redirect('/login');
}
});
app.get('/login', (req, res) => {
res.render('login');
});
app.post('/login', passport.authenticate('local', {
successRedirect: '/dashboard',
failureRedirect: '/login',
failureFlash: true
}));
app.get('/register', (req, res) => {
res.render('register');
});
app.post('/register', async (req, res) => {
const { username, password, name } = req.body;
try {
const existingUser = await db.User.findOne({ where: { username } });
if (existingUser) {
req.flash('error', 'Username already exists');
return res.redirect('/register');
}
await db.User.create({ username, password, name });
req.flash('success_msg', 'You are now registered and can log in');
res.redirect('/login');
} catch (err) {
console.error(err);
req.flash('error', 'Error registering user');
res.redirect('/register');
}
});
app.get('/logout', (req, res, next) => {
req.logout((err) => {
if (err) { return next(err); }
req.flash('success_msg', 'You are logged out');
res.redirect('/login');
});
});
app.get('/dashboard', ensureAuthenticated, async (req, res) => {
try {
const today = new Date();
const dateStr = today.toISOString().split('T')[0];
// Get daily totals
const nutrition = await utils.calculateDailyTotals(req.user.id, dateStr);
const water = await utils.calculateWaterTotal(req.user.id, dateStr);
// Get user goals
let goals = await db.UserGoal.findOne({ where: { UserId: req.user.id } });
if (!goals) {
goals = await db.UserGoal.create({
UserId: req.user.id,
target_protein_g: 150,
target_carbs_g: 200,
target_fat_g: 60,
target_water_ml: 2000
});
}
// Get weight info
const weightLogToday = await db.WeightLog.findOne({
where: { UserId: req.user.id, date: dateStr }
});
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];
const weightLogYesterday = await db.WeightLog.findOne({
where: { UserId: req.user.id, date: yesterdayStr }
});
let weightChange = null;
if (weightLogToday && weightLogYesterday) {
weightChange = weightLogToday.weight_kg - weightLogYesterday.weight_kg;
}
// Calculate remaining
const remaining = {
calories: req.user.target_daily_calories - nutrition.calories,
protein: goals.target_protein_g - nutrition.protein,
carbs: goals.target_carbs_g - nutrition.carbs,
fat: goals.target_fat_g - nutrition.fat,
water: goals.target_water_ml - water.total_ml
};
// Get macro percentages
const macroPercentages = utils.getMacroPercentages(
nutrition.protein,
nutrition.carbs,
nutrition.fat
);
// Get trends
const weightTrend = await utils.getWeightTrend(req.user.id, 7);
const calorieTrend = await utils.getCalorieTrend(req.user.id, 7);
// Suggestions
const suggestions = utils.suggestFoodsForMacros(
remaining.protein,
remaining.carbs,
remaining.fat
);
res.render('dashboard', {
nutrition,
water,
goals,
remaining,
macro_percentages: macroPercentages,
weight_today: weightLogToday,
weight_change: weightChange,
weight_trend: weightTrend,
calorie_trend: calorieTrend,
suggestions,
today: dateStr
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading dashboard');
res.render('dashboard', {
nutrition: { calories: 0, protein: 0, carbs: 0, fat: 0 },
water: { total_ml: 0 },
goals: { target_water_ml: 2000 },
remaining: { calories: 2000, protein: 150, carbs: 200, fat: 60, water: 2000 },
macro_percentages: { protein: 0, carbs: 0, fat: 0 },
weight_trend: [],
calorie_trend: [],
suggestions: [],
today: new Date().toISOString().split('T')[0]
});
}
});
app.get('/add-meal', ensureAuthenticated, (req, res) => {
res.render('add_meal', { today: new Date().toISOString().split('T')[0] });
});
app.post('/add-meal', ensureAuthenticated, async (req, res) => {
try {
let { date, meal_type, time, 'food_id[]': foodIds, 'quantity[]': quantities } = req.body;
if (!foodIds) {
req.flash('error_msg', 'Please add at least one food item');
return res.redirect('/add-meal');
}
// Normalize input (handle single item vs array)
if (!Array.isArray(foodIds)) {
foodIds = [foodIds];
quantities = [quantities];
}
const meal = await db.Meal.create({
UserId: req.user.id,
date: date || new Date(),
meal_type,
time: time || null
});
for (let i = 0; i < foodIds.length; i++) {
const foodId = foodIds[i];
const quantity = quantities[i];
if (foodId && quantity) {
const food = await db.FoodItem.findByPk(foodId);
if (food) {
await db.MealFood.create({
MealId: meal.id,
FoodItemId: foodId,
quantity: quantity,
calories_consumed: food.calories * quantity,
protein_consumed: food.protein_g * quantity,
carbs_consumed: food.carbs_g * quantity,
fat_consumed: food.fat_g * quantity
});
}
}
}
await utils.updateDailySummary(req.user.id, date);
req.flash('success_msg', 'Meal added successfully!');
res.redirect('/dashboard');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding meal');
res.redirect('/add-meal');
}
});
app.get('/api/search-food', ensureAuthenticated, async (req, res) => {
const query = req.query.q || '';
if (query.length < 2) {
return res.json([]);
}
try {
const results = await searchAllSources(query, apiClient);
res.json(results);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
app.post('/api/add-food', ensureAuthenticated, async (req, res) => {
try {
const data = req.body;
const food = await apiClient.saveFoodToDb(data);
if (food) {
res.json({
success: true,
food_id: food.id,
name: food.name
});
} else {
res.status(500).json({ success: false });
}
} catch (err) {
console.error(err);
res.status(500).json({ success: false });
}
});
app.post('/add-water', ensureAuthenticated, async (req, res) => {
try {
const amountMl = parseInt(req.body.amount_ml) || 250;
const date = req.body.date || new Date().toISOString().split('T')[0];
await db.WaterLog.create({
UserId: req.user.id,
date: date,
amount_ml: amountMl,
time: new Date()
});
await utils.updateDailySummary(req.user.id, date);
req.flash('success_msg', `Added ${amountMl}ml of water!`);
res.redirect('/dashboard');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding water');
res.redirect('/dashboard');
}
});
app.post('/add-weight', ensureAuthenticated, async (req, res) => {
try {
const weightKg = parseFloat(req.body.weight_kg);
const date = req.body.date || new Date().toISOString().split('T')[0];
let weightLog = await db.WeightLog.findOne({
where: { UserId: req.user.id, date: date }
});
if (weightLog) {
weightLog.weight_kg = weightKg;
weightLog.time = new Date();
await weightLog.save();
} else {
await db.WeightLog.create({
UserId: req.user.id,
date: date,
weight_kg: weightKg,
time: new Date()
});
}
await utils.updateDailySummary(req.user.id, date);
res.redirect('/dashboard');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding weight');
res.redirect('/dashboard');
}
});
app.get('/goals', ensureAuthenticated, async (req, res) => {
try {
const userGoals = await db.UserGoal.findOne({ where: { UserId: req.user.id } });
let bmr = null;
let tdee = null;
if (req.user.weight_kg && req.user.height_cm && req.user.age) {
bmr = utils.calculateBMR(
req.user.weight_kg,
req.user.height_cm,
req.user.age,
req.user.gender || 'male'
);
tdee = utils.calculateTDEE(bmr, req.user.activity_level || 'moderate');
}
res.render('goals', {
goals: userGoals,
bmr,
tdee
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading goals');
res.redirect('/dashboard');
}
});
app.post('/goals', ensureAuthenticated, async (req, res) => {
try {
// Update user info
const user = await db.User.findByPk(req.user.id);
user.age = parseInt(req.body.age) || 25;
user.gender = req.body.gender || 'male';
user.height_cm = parseFloat(req.body.height_cm) || 170;
user.weight_kg = parseFloat(req.body.weight_kg) || 70;
user.activity_level = req.body.activity_level || 'moderate';
// Calculate targets
const bmr = utils.calculateBMR(user.weight_kg, user.height_cm, user.age, user.gender);
const tdee = utils.calculateTDEE(bmr, user.activity_level);
const goalType = req.body.goal_type || 'recomp';
let targetCalories;
if (goalType === 'weight_loss') {
targetCalories = tdee - 500;
} else if (goalType === 'muscle_gain') {
targetCalories = tdee + 300;
} else {
targetCalories = tdee;
}
user.target_daily_calories = Math.round(targetCalories);
await user.save();
// Update or create goals
let userGoals = await db.UserGoal.findOne({ where: { UserId: req.user.id } });
if (!userGoals) {
userGoals = await db.UserGoal.create({ UserId: req.user.id });
}
userGoals.goal_type = goalType;
userGoals.target_weight_kg = parseFloat(req.body.target_weight_kg) || 70;
// Calculate macros
const macros = utils.calculateMacroTargets(user.weight_kg, goalType);
userGoals.target_protein_g = macros.protein_g;
userGoals.target_carbs_g = macros.carbs_g;
userGoals.target_fat_g = macros.fat_g;
userGoals.target_water_ml = parseInt(req.body.target_water_ml) || 2000;
await userGoals.save();
req.flash('success_msg', 'Goals updated successfully!');
res.redirect('/goals');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error updating goals');
res.redirect('/goals');
}
});
app.get('/foods', ensureAuthenticated, async (req, res) => {
try {
const category = req.query.category || 'all';
const searchQuery = req.query.q || '';
const whereClause = {};
if (category !== 'all') {
whereClause.category = category;
}
if (searchQuery) {
whereClause[db.Sequelize.Op.or] = [
{ name: { [db.Sequelize.Op.like]: `%${searchQuery}%` } },
{ name_tagalog: { [db.Sequelize.Op.like]: `%${searchQuery}%` } }
];
}
const filipinoWhere = { ...whereClause, is_filipino: true };
const otherWhere = { ...whereClause, is_filipino: false };
const filipinoFoods = await db.FoodItem.findAll({
where: filipinoWhere,
order: [['name', 'ASC']]
});
const otherFoods = await db.FoodItem.findAll({
where: otherWhere,
limit: 20,
order: [['name', 'ASC']]
});
const categories = ['all', 'kanin', 'ulam', 'sabaw', 'gulay', 'meryenda', 'almusal'];
res.render('foods', {
filipino_foods: filipinoFoods,
other_foods: otherFoods,
categories,
current_category: category,
search_query: searchQuery
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading foods');
res.redirect('/dashboard');
}
});
app.get('/progress', ensureAuthenticated, async (req, res) => {
try {
const days = parseInt(req.query.days) || 30;
const weightTrend = await utils.getWeightTrend(req.user.id, days);
const calorieTrend = await utils.getCalorieTrend(req.user.id, days);
let avgCalories = 0, avgProtein = 0, avgCarbs = 0, avgFat = 0;
if (calorieTrend.length > 0) {
avgCalories = calorieTrend.reduce((sum, d) => sum + d.calories, 0) / calorieTrend.length;
avgProtein = calorieTrend.reduce((sum, d) => sum + d.protein, 0) / calorieTrend.length;
avgCarbs = calorieTrend.reduce((sum, d) => sum + d.carbs, 0) / calorieTrend.length;
avgFat = calorieTrend.reduce((sum, d) => sum + d.fat, 0) / calorieTrend.length;
}
let weightChange = null;
if (weightTrend.length >= 2) {
weightChange = weightTrend[weightTrend.length - 1].weight_kg - weightTrend[0].weight_kg;
}
res.render('progress', {
weight_trend: weightTrend,
calorie_trend: calorieTrend,
avg_calories: Math.round(avgCalories),
avg_protein: Math.round(avgProtein),
avg_carbs: Math.round(avgCarbs),
avg_fat: Math.round(avgFat),
weight_change: weightChange,
days
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading progress');
res.redirect('/dashboard');
}
});
app.get('/meal-planner', ensureAuthenticated, async (req, res) => {
try {
const today = new Date();
// Calculate Monday of current week
const day = today.getDay();
const diff = today.getDate() - day + (day === 0 ? -6 : 1); // adjust when day is sunday
const startDate = new Date(today);
startDate.setDate(diff);
const dates = [];
for (let i = 0; i < 7; i++) {
const d = new Date(startDate);
d.setDate(startDate.getDate() + i);
dates.push(d);
}
const mealPlans = {};
for (const d of dates) {
const dateStr = d.toISOString().split('T')[0];
const plans = await db.MealPlan.findAll({
where: { UserId: req.user.id, date: dateStr },
include: [{
model: db.PlannedFood,
include: [db.FoodItem]
}]
});
mealPlans[dateStr] = plans.map(p => {
let calories = 0, protein = 0, carbs = 0, fat = 0;
const foods = [];
if (p.PlannedFoods) {
p.PlannedFoods.forEach(pf => {
if (pf.FoodItem) {
const q = pf.quantity || 1;
calories += pf.FoodItem.calories * q;
protein += pf.FoodItem.protein_g * q;
carbs += pf.FoodItem.carbs_g * q;
fat += pf.FoodItem.fat_g * q;
foods.push({
name: pf.FoodItem.name,
quantity: q
});
}
});
}
return {
id: p.id,
meal_type: p.meal_type,
is_completed: p.is_completed,
foods: foods,
totals: {
calories, protein, carbs, fat
}
};
});
}
res.render('meal_planner', {
dates: dates.map(d => d.toISOString().split('T')[0]),
meal_plans: mealPlans,
today: new Date().toISOString().split('T')[0]
});
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error loading meal planner');
res.redirect('/dashboard');
}
});
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;
if (!foodIds) {
req.flash('error_msg', 'Please add at least one food item');
return res.redirect('/meal-planner');
}
// Normalize input
if (!Array.isArray(foodIds)) {
foodIds = [foodIds];
quantities = [quantities];
}
// Find or create meal plan
let mealPlan = await db.MealPlan.findOne({
where: {
UserId: req.user.id,
date: date,
meal_type: meal_type
}
});
if (!mealPlan) {
mealPlan = await db.MealPlan.create({
UserId: req.user.id,
date: date,
meal_type: meal_type
});
}
// Add foods
for (let i = 0; i < foodIds.length; i++) {
const foodId = foodIds[i];
const quantity = quantities[i];
if (foodId && quantity) {
await db.PlannedFood.create({
MealPlanId: mealPlan.id,
FoodItemId: foodId,
quantity: quantity
});
}
}
req.flash('success_msg', 'Meal plan updated!');
res.redirect('/meal-planner');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error adding meal plan');
res.redirect('/meal-planner');
}
});
app.post('/meal-planner/complete/:id', ensureAuthenticated, async (req, res) => {
try {
const plan = await db.MealPlan.findOne({
where: { id: req.params.id, UserId: req.user.id }
});
if (plan) {
plan.is_completed = !plan.is_completed;
await plan.save();
// If marked complete, optionally create actual meal log?
// For now just toggle status as per request simplicity.
}
res.redirect('/meal-planner');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error updating plan status');
res.redirect('/meal-planner');
}
});
app.post('/meal-planner/delete/:id', ensureAuthenticated, async (req, res) => {
try {
await db.MealPlan.destroy({
where: { id: req.params.id, UserId: req.user.id }
});
req.flash('success_msg', 'Meal plan deleted');
res.redirect('/meal-planner');
} catch (err) {
console.error(err);
req.flash('error_msg', 'Error deleting meal plan');
res.redirect('/meal-planner');
}
});
// Database Sync and Server Start
const startServer = async () => {
try {
await db.sequelize.sync();
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));
} catch (err) {
console.error('Database connection error:', err);
}
};
startServer();