495 lines
16 KiB
Python
495 lines
16 KiB
Python
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session
|
|
from flask_login import LoginManager, login_user, logout_user, login_required, current_user
|
|
from werkzeug.security import generate_password_hash, check_password_hash
|
|
from datetime import date, datetime, timedelta
|
|
import os
|
|
|
|
from config import Config
|
|
from models import db, User, FoodItem, Meal, MealFood, WaterLog, WeightLog, MealPlan, PlannedFood, UserGoal
|
|
from api_client import NutritionAPI, search_all_sources
|
|
from utils import (
|
|
calculate_daily_totals, calculate_water_total, get_weight_trend,
|
|
get_calorie_trend, update_daily_summary, calculate_bmr, calculate_tdee,
|
|
calculate_macro_targets, get_macro_percentages, suggest_foods_for_macros
|
|
)
|
|
|
|
app = Flask(__name__)
|
|
app.config.from_object(Config)
|
|
|
|
# Initialize extensions
|
|
db.init_app(app)
|
|
login_manager = LoginManager()
|
|
login_manager.init_app(app)
|
|
login_manager.login_view = 'login'
|
|
|
|
# Initialize API client
|
|
api_client = NutritionAPI(app.config['API_NINJAS_KEY'])
|
|
|
|
@login_manager.user_loader
|
|
def load_user(user_id):
|
|
return User.query.get(int(user_id))
|
|
|
|
# ==================== ROUTES ====================
|
|
|
|
@app.route('/')
|
|
@login_required
|
|
def index():
|
|
"""Dashboard - Today's summary"""
|
|
today = date.today()
|
|
|
|
# Get daily totals
|
|
nutrition = calculate_daily_totals(current_user.id, today)
|
|
water = calculate_water_total(current_user.id, today)
|
|
|
|
# Get user goals
|
|
goals = UserGoal.query.filter_by(user_id=current_user.id).first()
|
|
if not goals:
|
|
goals = UserGoal(
|
|
user_id=current_user.id,
|
|
target_protein_g=150,
|
|
target_carbs_g=200,
|
|
target_fat_g=60,
|
|
target_water_ml=2000
|
|
)
|
|
db.session.add(goals)
|
|
db.session.commit()
|
|
|
|
# Get weight info
|
|
weight_log_today = WeightLog.query.filter_by(user_id=current_user.id, date=today).first()
|
|
weight_log_yesterday = WeightLog.query.filter_by(
|
|
user_id=current_user.id,
|
|
date=today - timedelta(days=1)
|
|
).first()
|
|
|
|
weight_change = None
|
|
if weight_log_today and weight_log_yesterday:
|
|
weight_change = weight_log_today.weight_kg - weight_log_yesterday.weight_kg
|
|
|
|
# Calculate remaining
|
|
remaining = {
|
|
'calories': current_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
|
|
macro_percentages = get_macro_percentages(
|
|
nutrition['protein'],
|
|
nutrition['carbs'],
|
|
nutrition['fat']
|
|
)
|
|
|
|
# Get trends
|
|
weight_trend = get_weight_trend(current_user.id, 7)
|
|
calorie_trend = get_calorie_trend(current_user.id, 7)
|
|
|
|
# Suggestions
|
|
suggestions = suggest_foods_for_macros(
|
|
remaining['protein'],
|
|
remaining['carbs'],
|
|
remaining['fat']
|
|
)
|
|
|
|
return render_template('dashboard.html',
|
|
nutrition=nutrition,
|
|
water=water,
|
|
goals=goals,
|
|
remaining=remaining,
|
|
macro_percentages=macro_percentages,
|
|
weight_today=weight_log_today,
|
|
weight_change=weight_change,
|
|
weight_trend=weight_trend,
|
|
calorie_trend=calorie_trend,
|
|
suggestions=suggestions,
|
|
today=today)
|
|
|
|
@app.route('/login', methods=['GET', 'POST'])
|
|
def login():
|
|
"""User login"""
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
|
|
user = User.query.filter_by(username=username).first()
|
|
|
|
if user and check_password_hash(user.password, password):
|
|
login_user(user)
|
|
return redirect(url_for('index'))
|
|
else:
|
|
flash('Invalid username or password', 'error')
|
|
|
|
return render_template('login.html')
|
|
|
|
@app.route('/register', methods=['GET', 'POST'])
|
|
def register():
|
|
"""User registration"""
|
|
if request.method == 'POST':
|
|
username = request.form.get('username')
|
|
password = request.form.get('password')
|
|
name = request.form.get('name')
|
|
|
|
if User.query.filter_by(username=username).first():
|
|
flash('Username already exists', 'error')
|
|
return redirect(url_for('register'))
|
|
|
|
user = User(
|
|
username=username,
|
|
password=generate_password_hash(password),
|
|
name=name
|
|
)
|
|
|
|
db.session.add(user)
|
|
db.session.commit()
|
|
|
|
flash('Registration successful! Please login.', 'success')
|
|
return redirect(url_for('login'))
|
|
|
|
return render_template('register.html')
|
|
|
|
@app.route('/logout')
|
|
@login_required
|
|
def logout():
|
|
"""User logout"""
|
|
logout_user()
|
|
return redirect(url_for('login'))
|
|
|
|
@app.route('/add-meal', methods=['GET', 'POST'])
|
|
@login_required
|
|
def add_meal():
|
|
"""Add a meal"""
|
|
if request.method == 'POST':
|
|
meal_date = request.form.get('date', date.today())
|
|
meal_type = request.form.get('meal_type')
|
|
meal_time = request.form.get('time')
|
|
|
|
# Convert date string to date object
|
|
if isinstance(meal_date, str):
|
|
meal_date = datetime.strptime(meal_date, '%Y-%m-%d').date()
|
|
|
|
# Convert time string to time object
|
|
meal_time_obj = None
|
|
if meal_time:
|
|
meal_time_obj = datetime.strptime(meal_time, '%H:%M').time()
|
|
|
|
# Create meal
|
|
meal = Meal(
|
|
user_id=current_user.id,
|
|
date=meal_date,
|
|
meal_type=meal_type,
|
|
time=meal_time_obj
|
|
)
|
|
db.session.add(meal)
|
|
db.session.flush() # Get meal ID
|
|
|
|
# Add foods to meal
|
|
food_ids = request.form.getlist('food_id[]')
|
|
quantities = request.form.getlist('quantity[]')
|
|
|
|
for food_id, quantity in zip(food_ids, quantities):
|
|
if food_id and quantity:
|
|
meal_food = MealFood(
|
|
meal_id=meal.id,
|
|
food_id=int(food_id),
|
|
quantity=float(quantity)
|
|
)
|
|
meal_food.calculate_nutrition()
|
|
db.session.add(meal_food)
|
|
|
|
db.session.commit()
|
|
update_daily_summary(current_user.id, meal_date)
|
|
|
|
flash('Meal added successfully!', 'success')
|
|
return redirect(url_for('index'))
|
|
|
|
# GET request - show form
|
|
return render_template('add_meal.html', today=date.today())
|
|
|
|
@app.route('/api/search-food')
|
|
@login_required
|
|
def api_search_food():
|
|
"""API endpoint for food search (AJAX)"""
|
|
query = request.args.get('q', '')
|
|
|
|
if len(query) < 2:
|
|
return jsonify([])
|
|
|
|
results = search_all_sources(query, api_client)
|
|
return jsonify(results)
|
|
|
|
@app.route('/api/add-food', methods=['POST'])
|
|
@login_required
|
|
def api_add_food():
|
|
"""API endpoint to add a food from API to database"""
|
|
data = request.json
|
|
|
|
# Save food to database
|
|
food = FoodItem(
|
|
name=data['name'],
|
|
calories=data['calories'],
|
|
protein_g=data.get('protein_g', 0),
|
|
carbs_g=data.get('carbs_g', 0),
|
|
fat_g=data.get('fat_g', 0),
|
|
serving_size_g=data.get('serving_size_g', 100),
|
|
serving_description=data.get('serving_description', '1 serving'),
|
|
source='api'
|
|
)
|
|
|
|
db.session.add(food)
|
|
db.session.commit()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'food_id': food.id,
|
|
'name': food.name
|
|
})
|
|
|
|
@app.route('/add-water', methods=['POST'])
|
|
@login_required
|
|
def add_water():
|
|
"""Add water intake"""
|
|
amount_ml = int(request.form.get('amount_ml', 250))
|
|
log_date = request.form.get('date', date.today())
|
|
|
|
if isinstance(log_date, str):
|
|
log_date = datetime.strptime(log_date, '%Y-%m-%d').date()
|
|
|
|
water_log = WaterLog(
|
|
user_id=current_user.id,
|
|
date=log_date,
|
|
amount_ml=amount_ml,
|
|
time=datetime.now().time()
|
|
)
|
|
|
|
db.session.add(water_log)
|
|
db.session.commit()
|
|
|
|
update_daily_summary(current_user.id, log_date)
|
|
|
|
flash(f'Added {amount_ml}ml of water!', 'success')
|
|
return redirect(url_for('index'))
|
|
|
|
@app.route('/add-weight', methods=['POST'])
|
|
@login_required
|
|
def add_weight():
|
|
"""Add weight log"""
|
|
weight_kg = float(request.form.get('weight_kg'))
|
|
log_date = request.form.get('date', date.today())
|
|
|
|
if isinstance(log_date, str):
|
|
log_date = datetime.strptime(log_date, '%Y-%m-%d').date()
|
|
|
|
# Check if weight log already exists for today
|
|
existing = WeightLog.query.filter_by(user_id=current_user.id, date=log_date).first()
|
|
|
|
if existing:
|
|
existing.weight_kg = weight_kg
|
|
existing.time = datetime.now().time()
|
|
else:
|
|
weight_log = WeightLog(
|
|
user_id=current_user.id,
|
|
date=log_date,
|
|
weight_kg=weight_kg,
|
|
time=datetime.now().time()
|
|
)
|
|
db.session.add(weight_log)
|
|
|
|
db.session.commit()
|
|
update_daily_summary(current_user.id, log_date)
|
|
|
|
flash(f'Weight logged: {weight_kg}kg', 'success')
|
|
return redirect(url_for('index'))
|
|
|
|
@app.route('/meal-planner')
|
|
@login_required
|
|
def meal_planner():
|
|
"""Meal planner page"""
|
|
# Get date range (current week)
|
|
today = date.today()
|
|
start_date = today - timedelta(days=today.weekday()) # Monday
|
|
dates = [start_date + timedelta(days=i) for i in range(7)]
|
|
|
|
# Get meal plans for the week
|
|
meal_plans = {}
|
|
for d in dates:
|
|
plans = MealPlan.query.filter_by(user_id=current_user.id, date=d).all()
|
|
meal_plans[d.isoformat()] = [
|
|
{
|
|
'id': p.id,
|
|
'meal_type': p.meal_type,
|
|
'is_completed': p.is_completed,
|
|
'foods': [
|
|
{
|
|
'name': pf.food.name,
|
|
'quantity': pf.quantity
|
|
}
|
|
for pf in p.foods
|
|
],
|
|
'totals': p.calculate_totals()
|
|
}
|
|
for p in plans
|
|
]
|
|
|
|
return render_template('meal_planner.html', dates=dates, meal_plans=meal_plans, today=today)
|
|
|
|
@app.route('/foods')
|
|
@login_required
|
|
def foods():
|
|
"""Food database page"""
|
|
category = request.args.get('category', 'all')
|
|
search_query = request.args.get('q', '')
|
|
|
|
query = FoodItem.query
|
|
|
|
if category != 'all':
|
|
query = query.filter_by(category=category)
|
|
|
|
if search_query:
|
|
query = query.filter(
|
|
db.or_(
|
|
FoodItem.name.ilike(f'%{search_query}%'),
|
|
FoodItem.name_tagalog.ilike(f'%{search_query}%')
|
|
)
|
|
)
|
|
|
|
# Filipino foods first
|
|
filipino_foods = query.filter_by(is_filipino=True).all()
|
|
other_foods = query.filter_by(is_filipino=False).limit(20).all()
|
|
|
|
categories = ['all', 'kanin', 'ulam', 'sabaw', 'gulay', 'meryenda', 'almusal']
|
|
|
|
return render_template('foods.html',
|
|
filipino_foods=filipino_foods,
|
|
other_foods=other_foods,
|
|
categories=categories,
|
|
current_category=category,
|
|
search_query=search_query)
|
|
|
|
@app.route('/progress')
|
|
@login_required
|
|
def progress():
|
|
"""Progress tracking page"""
|
|
days = int(request.args.get('days', 30))
|
|
|
|
weight_trend = get_weight_trend(current_user.id, days)
|
|
calorie_trend = get_calorie_trend(current_user.id, days)
|
|
|
|
# Calculate averages
|
|
if calorie_trend:
|
|
avg_calories = sum(d['calories'] for d in calorie_trend) / len(calorie_trend)
|
|
avg_protein = sum(d['protein'] for d in calorie_trend) / len(calorie_trend)
|
|
avg_carbs = sum(d['carbs'] for d in calorie_trend) / len(calorie_trend)
|
|
avg_fat = sum(d['fat'] for d in calorie_trend) / len(calorie_trend)
|
|
else:
|
|
avg_calories = avg_protein = avg_carbs = avg_fat = 0
|
|
|
|
# Weight change
|
|
weight_change = None
|
|
if len(weight_trend) >= 2:
|
|
weight_change = weight_trend[-1]['weight_kg'] - weight_trend[0]['weight_kg']
|
|
|
|
return render_template('progress.html',
|
|
weight_trend=weight_trend,
|
|
calorie_trend=calorie_trend,
|
|
avg_calories=avg_calories,
|
|
avg_protein=avg_protein,
|
|
avg_carbs=avg_carbs,
|
|
avg_fat=avg_fat,
|
|
weight_change=weight_change,
|
|
days=days)
|
|
|
|
@app.route('/goals', methods=['GET', 'POST'])
|
|
@login_required
|
|
def goals():
|
|
"""Goals and settings page"""
|
|
if request.method == 'POST':
|
|
# Update user info
|
|
current_user.age = int(request.form.get('age', 25))
|
|
current_user.gender = request.form.get('gender', 'male')
|
|
current_user.height_cm = float(request.form.get('height_cm', 170))
|
|
current_user.weight_kg = float(request.form.get('weight_kg', 70))
|
|
current_user.activity_level = request.form.get('activity_level', 'moderate')
|
|
|
|
# Calculate targets
|
|
bmr = calculate_bmr(
|
|
current_user.weight_kg,
|
|
current_user.height_cm,
|
|
current_user.age,
|
|
current_user.gender
|
|
)
|
|
tdee = calculate_tdee(bmr, current_user.activity_level)
|
|
|
|
# Get goal type
|
|
goal_type = request.form.get('goal_type', 'recomp')
|
|
|
|
# Adjust calories based on goal
|
|
if goal_type == 'weight_loss':
|
|
target_calories = tdee - 500
|
|
elif goal_type == 'muscle_gain':
|
|
target_calories = tdee + 300
|
|
else: # recomp
|
|
target_calories = tdee
|
|
|
|
current_user.target_daily_calories = int(target_calories)
|
|
|
|
# Update or create goals
|
|
user_goals = UserGoal.query.filter_by(user_id=current_user.id).first()
|
|
if not user_goals:
|
|
user_goals = UserGoal(user_id=current_user.id)
|
|
db.session.add(user_goals)
|
|
|
|
user_goals.goal_type = goal_type
|
|
user_goals.target_weight_kg = float(request.form.get('target_weight_kg', 70))
|
|
|
|
# Calculate macros
|
|
macros = calculate_macro_targets(current_user.weight_kg, goal_type)
|
|
user_goals.target_protein_g = macros['protein_g']
|
|
user_goals.target_carbs_g = macros['carbs_g']
|
|
user_goals.target_fat_g = macros['fat_g']
|
|
user_goals.target_water_ml = int(request.form.get('target_water_ml', 2000))
|
|
|
|
db.session.commit()
|
|
|
|
flash('Goals updated successfully!', 'success')
|
|
return redirect(url_for('goals'))
|
|
|
|
# GET request
|
|
user_goals = UserGoal.query.filter_by(user_id=current_user.id).first()
|
|
|
|
# Calculate current BMR and TDEE
|
|
bmr = tdee = None
|
|
if current_user.weight_kg and current_user.height_cm and current_user.age:
|
|
bmr = calculate_bmr(
|
|
current_user.weight_kg,
|
|
current_user.height_cm,
|
|
current_user.age,
|
|
current_user.gender or 'male'
|
|
)
|
|
tdee = calculate_tdee(bmr, current_user.activity_level or 'moderate')
|
|
|
|
return render_template('goals.html',
|
|
user=current_user,
|
|
goals=user_goals,
|
|
bmr=bmr,
|
|
tdee=tdee)
|
|
|
|
# ==================== DATABASE INITIALIZATION ====================
|
|
|
|
@app.cli.command()
|
|
def init_db():
|
|
"""Initialize the database"""
|
|
db.create_all()
|
|
print("Database initialized!")
|
|
|
|
@app.cli.command()
|
|
def seed_db():
|
|
"""Seed the database with Filipino foods"""
|
|
from seed_data import seed_filipino_foods
|
|
seed_filipino_foods()
|
|
|
|
if __name__ == '__main__':
|
|
with app.app_context():
|
|
db.create_all()
|
|
app.run(debug=True, host='127.0.0.1', port=5001)
|