3 Commits

Author SHA1 Message Date
Stef
bc6b95f781 Development (#13)
* Adjusted GUI of daily dashboard to better deal with float values

* Change password + New base (#10)

* created a new Base template to test with

* Changed out the base and added a new password page

* Password change works, UI needs redisgn

* Daily log now sums up amount per item in a day

* Quick fix for url name issues
2025-10-18 14:52:54 +02:00
312eda85df Can now set macro target 2025-10-10 19:50:18 +02:00
Stef
e934633370 Development (#12)
* Adjusted GUI of daily dashboard to better deal with float values

* Change password + New base (#10)

* created a new Base template to test with

* Changed out the base and added a new password page

* Password change works, UI needs redisgn

* Daily log now sums up amount per item in a day
2025-10-08 19:12:19 +02:00
10 changed files with 186 additions and 29 deletions

View File

@@ -49,8 +49,9 @@ def find_item():
return render_template("find_item.html") return render_template("find_item.html")
# TODO: Switch from this to query parameters / url args
@date_present @date_present
@bp.route("/select_item/<string:input>", methods=["GET"]) @bp.route("/select_item/<path:input>", methods=["GET"])
def select_item(input: str): def select_item(input: str):
# Check if input is a barcode # Check if input is a barcode
if input.isdigit(): if input.isdigit():
@@ -69,7 +70,7 @@ def select_item(input: str):
@date_present @date_present
@bp.route("/add_new_item/<string:input>", methods=["GET"]) @bp.route("/add_new_item/<path:input>", methods=["GET"])
def add_new_item(input: str): def add_new_item(input: str):
form = FoodItemForm() form = FoodItemForm()
@@ -81,7 +82,7 @@ def add_new_item(input: str):
@date_present @date_present
@bp.route("/add_new_item/<string:input>", methods=["POST"]) @bp.route("/add_new_item/<path:input>", methods=["POST"])
def post_new_item(input: str): def post_new_item(input: str):
form = FoodItemForm() form = FoodItemForm()

View File

@@ -17,15 +17,12 @@
</li> </li>
</ul> </ul>
<ul class="navbar-nav"> <ul class="navbar-nav">
{% if current_user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a> <a class="nav-link" href="{{ url_for('auth.logout') }}">Logout</a>
</li> </li>
{% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a> <a class="nav-link" href="{{ url_for('auth.logout') }}">Set</a>
</li> </li>
{% endif %}
<li class="nav-item"> <li class="nav-item">
<button id="toggleTheme" class="btn btn-outline-light">Toggle Theme</button> <button id="toggleTheme" class="btn btn-outline-light">Toggle Theme</button>
</li> </li>

View File

@@ -24,6 +24,7 @@
</a> </a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdown"> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdown">
<li><a class="dropdown-item" href='{{ url_for("auth.change_pass") }}'>Change Password</a></li> <li><a class="dropdown-item" href='{{ url_for("auth.change_pass") }}'>Change Password</a></li>
<li><a class="dropdown-item" href='{{ url_for("user.set_macros") }}'>Set macros</a></li>
<li><a class="dropdown-item" href="#">Profile</a></li> <li><a class="dropdown-item" href="#">Profile</a></li>
<li> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">

View File

@@ -9,12 +9,13 @@ from flask import (
) )
from flask_login import current_user from flask_login import current_user
from application import db from application import db
from forms import FoodItemForm from forms import FoodItemForm, MacroForm
from models import FoodItem, FoodLog from models import FoodItem, FoodLog
from datetime import datetime from datetime import datetime
from application.utils import login_required, macro_arr_to_json from application.utils import login_required, macro_arr_to_json
from numpy import array from numpy import array
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from sqlalchemy import func
user_bp = Blueprint( user_bp = Blueprint(
"user", "user",
@@ -40,14 +41,23 @@ def daily_log():
session["selected_date"] = today.isoformat() session["selected_date"] = today.isoformat()
# Get logs from today # Get logs from today
logs_today = current_user.food_logs.filter_by( logs_today = (
date_=today.isoformat() current_user.food_logs.join(
).all() FoodItem, FoodItem.id == FoodLog.food_item_id
)
.filter(FoodLog.date_ == today)
.group_by(FoodItem.id)
.with_entities(
FoodItem, # the full object
func.sum(FoodLog.amount).label("total_amount"),
)
.all()
)
# calculate macros # calculate macros
macros = array((0.0, 0.0, 0.0, 0.0)) macros = array((0.0, 0.0, 0.0, 0.0))
for log in logs_today: for food_item, amount in logs_today:
macros += array(log.food_item.macros()) / 100 * log.amount macros += array(food_item.macros()) / 100 * amount
macros = macro_arr_to_json(macros.tolist()) macros = macro_arr_to_json(macros.tolist())
# Render HTML # Render HTML
@@ -107,3 +117,20 @@ def remove_log(id: int):
db.session.delete(log) db.session.delete(log)
db.session.commit() db.session.commit()
return redirect(url_for("user.daily_log")) return redirect(url_for("user.daily_log"))
@user_bp.route("/set_macros", methods=["GET", "POST"])
def set_macros():
form = MacroForm()
if form.validate_on_submit():
current_user.set_macros(
form.protein.data,
form.carbohydrates.data,
form.fat.data,
form.calories.data,
)
db.session.commit()
return redirect(url_for("user.daily_log"))
return render_template("settings.html", form=form)

View File

@@ -29,22 +29,22 @@
<div class="card p-3"> <div class="card p-3">
<h5>Items Eaten Today</h5> <h5>Items Eaten Today</h5>
<div class="list-group list-group-flush"> <div class="list-group list-group-flush">
{% for log in logs %} {% for food_item, amount in logs %}
<div class="list-group-item d-flex align-items-center"> <div class="list-group-item d-flex align-items-center">
<!-- Weight: fixed width, right-aligned --> <!-- Weight: fixed width, right-aligned -->
<span class="text-end" style="width: 6ch; flex-shrink: 0;"> <span class="text-end" style="width: 6ch; flex-shrink: 0;">
({{ log.amount | int }}g) ({{ amount | int }}g)
</span> </span>
<!-- Food name: flexible, truncates if too long --> <!-- Food name: flexible, truncates if too long -->
<span class="text-truncate flex-grow-1" <span class="text-truncate flex-grow-1"
style="min-width: 0; margin-left: 0.5rem; margin-right: 0.5rem;"> style="min-width: 0; margin-left: 0.5rem; margin-right: 0.5rem;">
{{ log.food_item.name }} {{ food_item.name }}
</span> </span>
<!-- kcal: fixed width, right-aligned, pushed to the right --> <!-- kcal: fixed width, right-aligned, pushed to the right -->
<span class="d-inline-block text-end ms-auto" style="width: 9ch; flex-shrink: 0;"> <span class="d-inline-block text-end ms-auto" style="width: 9ch; flex-shrink: 0;">
{{ (log.food_item.energy_100 * log.amount / 100) | int }} kcal {{ (food_item.energy_100 * amount / 100) | int }} kcal
</span> </span>
</div> </div>
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,30 @@
{% extends "base.html"%}
{% block title %} Daily Macro Settings {% endblock %}
{% block content %}
<div class="container py-5">
<h2 class="mb-4 text-center">Set Your Daily Macro Targets</h2>
<form action="{{ url_for('user.set_macros') }}" method="POST" class="card p-4 shadow-sm bg-body-secondary">
{{ form.hidden_tag() }}
<div class="mb-3">
<label for="protein" class="form-label">Protein (g)</label>
{{form.protein(class="form-control")}}
</div>
<div class="mb-3">
<label for="carbs" class="form-label">Carbohydrates (g)</label>
{{form.carbohydrates(class="form-control")}}
</div>
<div class="mb-3">
<label for="fat" class="form-label">Fat (g)</label>
{{form.fat(class="form-control")}}
</div>
<div class="mb-3">
<label for="calories" class="form-label">Calories (kcal)</label>
{{form.calories(class="form-control")}}
</div>
{{form.submit(class="btn btn-primary")}}
</form>
</div>
{% endblock %}

View File

@@ -41,9 +41,11 @@ def macro_arr_to_json(data: list[float]):
{ {
"name": "Calories", "name": "Calories",
"current": cal, "current": cal,
"target": 2000, "target": current_user.calories,
"bar_width": 100 - abs(cal / 20 - 100), "bar_width": 100 - abs(cal * 100 / current_user.calories - 100),
"bar_width_overflow": max(0, cal / 20 - 100), "bar_width_overflow": max(
0, cal * 100 / current_user.calories - 100
),
"unit": " kcal", "unit": " kcal",
"color": "bg-calories", "color": "bg-calories",
"overflow_color": "bg-calories-dark", "overflow_color": "bg-calories-dark",
@@ -51,9 +53,11 @@ def macro_arr_to_json(data: list[float]):
{ {
"name": "Protein", "name": "Protein",
"current": pro, "current": pro,
"target": 150, "target": current_user.protein,
"bar_width": 100 - abs(pro / 1.5 - 100), "bar_width": 100 - abs(pro * 100 / current_user.protein - 100),
"bar_width_overflow": max(0, pro / 1.5 - 100), "bar_width_overflow": max(
0, pro * 100 / current_user.protein - 100
),
"unit": "g", "unit": "g",
"color": "bg-protein", "color": "bg-protein",
"overflow_color": "bg-protein-dark", "overflow_color": "bg-protein-dark",
@@ -61,9 +65,12 @@ def macro_arr_to_json(data: list[float]):
{ {
"name": "Carbs", "name": "Carbs",
"current": car, "current": car,
"target": 250, "target": current_user.carbohydrates,
"bar_width": 100 - abs(car / 2.5 - 100), "bar_width": 100
"bar_width_overflow": max(0, car / 2.5 - 100), - abs(car * 100 / current_user.carbohydrates - 100),
"bar_width_overflow": max(
0, car * 100 / current_user.carbohydrates - 100
),
"unit": "g", "unit": "g",
"color": "bg-carbs", "color": "bg-carbs",
"overflow_color": "bg-carbs-dark", "overflow_color": "bg-carbs-dark",
@@ -71,9 +78,9 @@ def macro_arr_to_json(data: list[float]):
{ {
"name": "Fat", "name": "Fat",
"current": fat, "current": fat,
"target": 70, "target": current_user.fat,
"bar_width": 100 - abs(fat / 0.7 - 100), "bar_width": 100 - abs(fat * 100 / current_user.fat - 100),
"bar_width_overflow": max(0, fat / 0.7 - 100), "bar_width_overflow": max(0, fat * 100 / current_user.fat - 100),
"unit": "g", "unit": "g",
"color": "bg-fat", "color": "bg-fat",
"overflow_color": "bg-fat-dark", "overflow_color": "bg-fat-dark",

View File

@@ -68,6 +68,30 @@ class FoodItemForm(FlaskForm):
submit = SubmitField("Add Item") submit = SubmitField("Add Item")
class MacroForm(FlaskForm):
protein = FloatField(
"Protein (g)",
validators=[InputRequired()],
render_kw={"inputmode": "decimal"},
)
carbohydrates = FloatField(
"Carbohydrates (g)",
validators=[InputRequired()],
render_kw={"inputmode": "decimal"},
)
fat = FloatField(
"Fat (g)",
validators=[InputRequired()],
render_kw={"inputmode": "decimal"},
)
calories = FloatField(
"Calories (kcal)",
validators=[InputRequired()],
render_kw={"inputmode": "decimal"},
)
submit = SubmitField("Update macros")
class FoodLogForm(FlaskForm): class FoodLogForm(FlaskForm):
amount = FloatField("amount of food (g/ml)", validators=[DataRequired()]) amount = FloatField("amount of food (g/ml)", validators=[DataRequired()])
submit = SubmitField("Log Item") submit = SubmitField("Log Item")

View File

@@ -0,0 +1,56 @@
"""empty message
Revision ID: 21ec41b645e9
Revises: 65eaeafb0904
Create Date: 2025-10-10 19:26:21.718736
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "21ec41b645e9"
down_revision = "65eaeafb0904"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"protein", sa.Float(), nullable=False, server_default="100"
)
)
batch_op.add_column(
sa.Column(
"carbohydrates",
sa.Float(),
nullable=False,
server_default="250",
)
)
batch_op.add_column(
sa.Column("fat", sa.Float(), nullable=False, server_default="60")
)
batch_op.add_column(
sa.Column(
"calories", sa.Float(), nullable=False, server_default="2000"
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("user", schema=None) as batch_op:
batch_op.drop_column("calories")
batch_op.drop_column("fat")
batch_op.drop_column("carbohydrates")
batch_op.drop_column("protein")
# ### end Alembic commands ###

View File

@@ -16,6 +16,11 @@ class User(UserMixin, db.Model):
is_admin = db.Column(db.Boolean, nullable=False, default=False) is_admin = db.Column(db.Boolean, nullable=False, default=False)
must_change_password = db.Column(db.Boolean, nullable=False, default=False) must_change_password = db.Column(db.Boolean, nullable=False, default=False)
protein = db.Column(db.Float, nullable=False)
carbohydrates = db.Column(db.Float, nullable=False)
fat = db.Column(db.Float, nullable=False)
calories = db.Column(db.Float, nullable=False)
food_items = db.relationship("FoodItem", lazy="dynamic", backref="user") food_items = db.relationship("FoodItem", lazy="dynamic", backref="user")
food_logs = db.relationship("FoodLog", lazy="dynamic", backref="user") food_logs = db.relationship("FoodLog", lazy="dynamic", backref="user")
@@ -32,6 +37,15 @@ class User(UserMixin, db.Model):
self.is_admin = is_admin self.is_admin = is_admin
self.must_change_password = must_change_password self.must_change_password = must_change_password
def set_macros(
self, protein: float, carbs: float, fat: float, calories: float
):
self.protein = protein
self.carbohydrates = carbs
self.fat = fat
self.calories = calories
return
def check_password(self, password: str) -> bool: def check_password(self, password: str) -> bool:
return check_password_hash(pwhash=self.password, password=password) return check_password_hash(pwhash=self.password, password=password)