7 Commits

Author SHA1 Message Date
4bc319c32a Revert "Update requirements.txt"
This reverts commit 24a1757166.
2025-08-21 17:05:32 +02:00
24a1757166 Update requirements.txt 2025-08-21 16:55:52 +02:00
5373d373ca Update find_item.html 2025-08-14 17:40:27 +02:00
Stef
dca3ff7efe Merge pull request #8 from StefBuwalda/development
Update find_item.html
2025-08-14 17:38:25 +02:00
7cedd8f74d Update find_item.html 2025-08-14 17:38:10 +02:00
Stef
faff1a60b8 Merge pull request #7 from StefBuwalda/development
Refactor add meal flow and remove v2 routes
2025-08-14 15:50:37 +02:00
73985b9b6d Refactor add meal flow and remove v2 routes
Migrated add_meal_v2 routes and templates to add_meal, renaming endpoints and updating references throughout the app. Removed legacy daily_log2 route and template, consolidating to daily_log. Moved macro_arr_to_json to utils.py for reuse. Updated navigation and redirect logic to use new routes. Improved item finding and barcode scanning UI.
2025-08-14 15:50:17 +02:00
10 changed files with 240 additions and 245 deletions

11
app.py
View File

@@ -12,7 +12,7 @@ from application import db, app, login_manager
from application.admin.routes import admin_bp from application.admin.routes import admin_bp
from application.user.routes import user_bp from application.user.routes import user_bp
from application.auth.routes import bp as auth_bp from application.auth.routes import bp as auth_bp
from application.add_meal_v2.routes import bp as add_meal_v2_bp from application.add_meal.routes import bp as add_meal_bp
from typing import Optional from typing import Optional
# Config # Config
@@ -30,12 +30,17 @@ def load_user(user_id: int):
app.register_blueprint(admin_bp) app.register_blueprint(admin_bp)
app.register_blueprint(user_bp) app.register_blueprint(user_bp)
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(add_meal_v2_bp) app.register_blueprint(add_meal_bp)
# @app.errorhandler(404)
# def page_not_found(e):
# return redirect("/")
# Routes # Routes
def default_return(next_page: Optional[str] = None): def default_return(next_page: Optional[str] = None):
return redirect(url_for("user.daily_log2")) return redirect(url_for("user.daily_log"))
if next_page: if next_page:
return redirect(next_page) return redirect(next_page)
if current_user.is_admin: if current_user.is_admin:

View File

@@ -17,9 +17,9 @@ from sqlalchemy.sql.elements import BinaryExpression
from typing import cast from typing import cast
bp = Blueprint( bp = Blueprint(
"add_meal_v2", "add_meal",
__name__, __name__,
url_prefix="/add_meal_v2", url_prefix="/add_meal",
template_folder="templates", template_folder="templates",
) )
@@ -27,14 +27,14 @@ bp = Blueprint(
def date_present(func): def date_present(func):
def check_date(): def check_date():
if "selected_date" not in session: if "selected_date" not in session:
return redirect(url_for("user.daily_log2")) return redirect(url_for("user.daily_log"))
def item_selected(func): def item_selected(func):
def check_item(): def check_item():
if check_item(): if check_item():
if "item_id" not in session: if "item_id" not in session:
return redirect(url_for("add_meal_v2.get_barcode")) return redirect(url_for("add_meal.find_item"))
@bp.before_request @bp.before_request
@@ -44,14 +44,14 @@ def login_required():
@date_present @date_present
@bp.route("/get_barcode", methods=["GET"]) @bp.route("/find_item", methods=["GET"])
def get_barcode(): def find_item():
return render_template("scan_barcode_v2.html") return render_template("find_item.html")
@date_present @date_present
@bp.route("/add_existing/<string:input>", methods=["GET"]) @bp.route("/select_item/<string:input>", methods=["GET"])
def add_existing(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():
item = current_user.food_items.filter_by(barcode=input).first() item = current_user.food_items.filter_by(barcode=input).first()
@@ -61,28 +61,28 @@ def add_existing(input: str):
if item is None: if item is None:
# Does not exist, add item # Does not exist, add item
return redirect(url_for("add_meal_v2.add_new", input=input)) return redirect(url_for("add_meal.add_new_item", input=input))
# Track item to add and continue to next step # Track item to add and continue to next step
session["item_id"] = item.id session["item_id"] = item.id
return redirect(url_for("add_meal_v2.step4")) return redirect(url_for("add_meal.step4"))
@date_present @date_present
@bp.route("/add_new/<string:input>", methods=["GET"]) @bp.route("/add_new_item/<string:input>", methods=["GET"])
def add_new(input: str): def add_new_item(input: str):
form = FoodItemForm() form = FoodItemForm()
if input.isdigit(): if input.isdigit():
form.barcode.data = input form.barcode.data = input
else: else:
form.name.data = input form.name.data = input
return render_template("add_item.html", form=form) return render_template("add_new_item.html", form=form)
@date_present @date_present
@bp.route("/add_new/<string:input>", methods=["POST"]) @bp.route("/add_new_item/<string:input>", methods=["POST"])
def add_new_post(input: str): def post_new_item(input: str):
form = FoodItemForm() form = FoodItemForm()
if form.validate_on_submit(): if form.validate_on_submit():
@@ -126,10 +126,10 @@ def add_new_post(input: str):
print(f"Item exists: {item.barcode} {item.name}") print(f"Item exists: {item.barcode} {item.name}")
# Item added or already present, return to step 3. # Item added or already present, return to step 3.
return redirect(url_for("add_meal_v2.add_existing", input=input)) return redirect(url_for("add_meal.select_item", input=input))
else: else:
print("[DEBUG] Form Invalid") print("[DEBUG] Form Invalid")
return redirect(url_for("add_meal_v2.add_new", input=input)) return redirect(url_for("add_meal.add_new_item", input=input))
@date_present @date_present
@@ -139,7 +139,7 @@ def step4():
form = FoodLogForm() form = FoodLogForm()
item = db.session.get(FoodItem, session["item_id"]) item = db.session.get(FoodItem, session["item_id"])
if not item: if not item:
return redirect(url_for("add_meal_v2.get_barcode")) return redirect(url_for("add_meal.find_item"))
if form.validate_on_submit(): if form.validate_on_submit():
assert form.amount.data assert form.amount.data
@@ -156,9 +156,9 @@ def step4():
db.session.commit() db.session.commit()
session.pop("item_id") session.pop("item_id")
session.pop("selected_date") session.pop("selected_date")
return redirect(url_for("user.daily_log2")) return redirect(url_for("user.daily_log"))
return render_template("step4.html", tod="idk", item=item, form=form) return render_template("step4.html", item=item, form=form)
@date_present @date_present

View File

@@ -0,0 +1,126 @@
{% extends "base.html" %}
{% block content %}
<div class="container d-flex flex-column justify-content-start align-items-center py-4" style="min-height: 100vh;">
<div class="card shadow-sm w-100">
<div class="card-body d-flex flex-column">
<h5 class="card-title text-center mb-4">Item Scanner</h5>
<video id="camera" autoplay class="w-100 mb-3" muted></video>
<div class="mb-3">
<label for="manualSearch" class="form-label">Or search manually</label>
<input type="text" class="form-control" id="manualSearch" placeholder="Enter item name">
</div>
<ul class="list-group mb-3" id="searchResults"></ul>
<button class="btn btn-primary w-100" id="createItemBtn">Create New Item</button>
</div>
</div>
</div>
{% endblock%}
{% block scripts %}
<script>
const baseURL = "{{url_for('add_meal.select_item', input='!')}}";
const baseURL2 = "{{url_for('add_meal.add_new_item', input='!')}}";
</script>
<script type="module">
// Import barcode scanner
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
// constants
const codeReader = new BrowserMultiFormatReader();
const videoElement = document.getElementById('camera');
// Start async camera thingymibob
document.addEventListener('DOMContentLoaded', async () => {
console.log('[DEBUG] Page loaded, starting barcode scan');
try {
await navigator.mediaDevices.getUserMedia({ video: true });
} catch (err) {
alert("No camera found or no camera permission");
console.error("Could not access the camera:", err);
return;
}
console.log('[DEBUG] Permission given and at least one device present');
const devices = await codeReader.listVideoInputDevices();
console.log('[DEBUG] Cameras found:', devices);
const rearCamera = devices.find(device => device.label.toLowerCase().includes('back'))
|| devices.find(device => device.label.toLowerCase().includes('rear'))
|| devices[0]; // fallback
const selectedDeviceId = rearCamera?.deviceId;
await codeReader.decodeFromVideoDevice(selectedDeviceId, videoElement, async (result, err) => {
if (result) {
// Result found, redirect to the URL
console.log(result)
const codeText = result.getText();
window.location.href = baseURL.replace("!", encodeURIComponent(codeText));
}
});
});
</script>
<script>
const searchInput = document.getElementById('manualSearch');
const resultsList = document.getElementById('searchResults');
const createBtn = document.getElementById('createItemBtn');
let controller; // keeps track of the current fetch
// TODO: Debounce input, wait for user to stop typing (f.e. 300ms)
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
// Check if there is enough input to fetch
if (query.length < 2) {
resultsList.innerHTML = '';
return;
}
if (controller) controller.abort(); // Abort previous fetch if still running
controller = new AbortController(); // new controller for this fetch
const signal = controller.signal; // Signal to fetch to listen for aborts
fetch(`{{url_for("add_meal.query")}}?q=${encodeURIComponent(query)}`, { signal })
.then(response => response.json())
.then(data => {
resultsList.innerHTML = ''; // clear before appending
data.forEach(item => {
// Create list item with button inside that changes the hidden form and submits it
const li = document.createElement('li');
li.className = 'list-group-item p-0';
const btn = document.createElement('button');
btn.className = 'list-group-item list-group-item-action m-0 border-0';
btn.style.width = '100%'; // make it fill the li
btn.textContent = item;
btn.addEventListener('click', () => {
window.location.href = baseURL.replace("!", encodeURIComponent(item));
});
li.appendChild(btn);
resultsList.appendChild(li);
});
}).catch(err => {
if (err.name == 'AbortError') {
console.log("Fetch aborted");
}
if (err.name !== 'AbortError') {
console.error('Fetch error:', err);
}
});
});
</script>
<script>
createBtn.addEventListener('click', () => {
const newItem = searchInput.value.trim()
window.location.href = newItem ? baseURL2.replace("!", encodeURIComponent(newItem)) : baseURL2;
});
</script>
{% endblock %}

View File

@@ -1,7 +1,6 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<p>{{ tod }}</p>
<p>{{ item.name }}</p> <p>{{ item.name }}</p>
<form method="POST"> <form method="POST">

View File

@@ -1,134 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-5">
<!-- Header -->
<div class="text-center mb-4">
<h1 class="fw-bold">Barcode Scanner</h1>
<p class="text-muted">Use your camera to scan barcodes</p>
</div>
<!-- Video preview -->
<div class="d-flex justify-content-center mb-4">
<video id="video" class="border rounded shadow-sm" style="width: 100%; max-width: 500px;" autoplay
muted></video>
</div>
<!-- Start/Stop buttons -->
<div class="d-flex justify-content-center gap-3 mb-4">
<button id="startButton" class="btn btn-primary px-4">Start Scanning</button>
<button id="stopButton" class="btn btn-danger px-4">Stop</button>
</div>
<!-- Search box and suggestions -->
<div class="d-flex justify-content-center mt-4">
<div class="w-100 position-relative" style="max-width: 500px;">
<!-- Input group (search + go button) -->
<div class="input-group">
<input type="text" id="search-box" class="form-control" placeholder="Search..." autocomplete="off">
<button id="go-button" class="btn btn-success">Go</button>
</div>
<!-- Suggestions -->
<ul id="suggestions" class="list-group position-absolute w-100 mt-1" style="z-index: 1000;"></ul>
</div>
</div>
<!-- Suggestions list -->
<div class="d-flex justify-content-center">
<ul id="suggestions" class="list-group position-absolute mt-1 w-100" style="max-width: 500px; z-index: 1000;">
</ul>
</div>
</div>
<script>
const searchBox = document.getElementById('search-box');
const suggestionsBox = document.getElementById('suggestions');
const goButton = document.getElementById('go-button');
searchBox.addEventListener('input', function () {
const query = searchBox.value;
if (query.length < 2) {
suggestionsBox.innerHTML = '';
return;
}
fetch(`{{url_for("add_meal_v2.query")}}?q=${encodeURIComponent(query)}`)
.then(response => response.json())
.then(data => {
suggestionsBox.innerHTML = '';
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
// Apply Bootstrap classes
li.classList.add('list-group-item', 'list-group-item-action', 'cursor-pointer');
// Add click behavior
li.addEventListener('click', () => {
searchBox.value = item;
suggestionsBox.innerHTML = '';
});
// Add to suggestions box
suggestionsBox.appendChild(li);
});
});
});
// ✅ Redirect when the "Go" button is clicked
goButton.addEventListener('click', () => {
const value = searchBox.value.trim();
if (value) {
const baseURL = "{{url_for('add_meal_v2.add_existing', input='!')}}";
window.location.href = baseURL.replace("!", encodeURIComponent(value));
}
});
</script>
<script type="module">
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
// constants
const codeReader = new BrowserMultiFormatReader();
const videoElement = document.getElementById('video');
// Start scanning for barcode
document.getElementById('startButton').addEventListener('click', async () => {
console.log('[DEBUG] Start button clicked')
try {
await navigator.mediaDevices.getUserMedia({ video: true });
} catch (err) {
alert("No camera found or no camera permission");
console.error("Could not access the camera:", err);
return;
}
console.log('[DEBUG] Permission given and at least one device present');
const devices = await codeReader.listVideoInputDevices();
console.log('[DEBUG] Cameras found:', devices);
const rearCamera = devices.find(device => device.label.toLowerCase().includes('back'))
|| devices.find(device => device.label.toLowerCase().includes('rear'))
|| devices[0]; // fallback
const selectedDeviceId = rearCamera?.deviceId;
await codeReader.decodeFromVideoDevice(selectedDeviceId, videoElement, async (result, err) => {
if (result) {
// Result found, this should post the barcode
const codeText = result.getText();
const baseURL = "{{url_for('add_meal_v2.add_existing', input='!')}}";
window.location.href = baseURL.replace("!", encodeURIComponent(codeText));
}
})
})
document.getElementById('stopButton').addEventListener('click', () => {
codeReader.reset();
});
</script>
{% endblock %}

View File

@@ -23,7 +23,7 @@
<div class="d-flex w-100"> <div class="d-flex w-100">
<ul class="navbar-nav flex-grow-1"> <ul class="navbar-nav flex-grow-1">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('user.daily_log2') }}">Daily Log (new)</a> <a class="nav-link" href="{{ url_for('user.daily_log') }}">Daily Log (new)</a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('user.dashboard') }}">Dashboard</a> <a class="nav-link" href="{{ url_for('user.dashboard') }}">Dashboard</a>

View File

@@ -12,7 +12,7 @@ from application import db
from forms import FoodItemForm from forms import FoodItemForm
from models import FoodItem, FoodLog from models import FoodItem, FoodLog
from datetime import datetime from datetime import datetime
from application.utils import login_required 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
@@ -22,58 +22,6 @@ user_bp = Blueprint(
template_folder="templates", template_folder="templates",
) )
def macro_arr_to_json(data: list[float]):
assert len(data) == 4
cal = data[0]
pro = data[3]
car = data[2]
fat = data[1]
macros = [
{
"name": "Calories",
"current": cal,
"target": 2000,
"bar_width": 100 - abs(cal / 20 - 100),
"bar_width_overflow": max(0, cal / 20 - 100),
"unit": " kcal",
"color": "bg-calories",
"overflow_color": "bg-calories-dark",
},
{
"name": "Protein",
"current": pro,
"target": 150,
"bar_width": 100 - abs(pro / 1.5 - 100),
"bar_width_overflow": max(0, pro / 1.5 - 100),
"unit": "g",
"color": "bg-protein",
"overflow_color": "bg-protein-dark",
},
{
"name": "Carbs",
"current": car,
"target": 250,
"bar_width": 100 - abs(car / 2.5 - 100),
"bar_width_overflow": max(0, car / 2.5 - 100),
"unit": "g",
"color": "bg-carbs",
"overflow_color": "bg-carbs-dark",
},
{
"name": "Fat",
"current": fat,
"target": 70,
"bar_width": 100 - abs(fat / 0.7 - 100),
"bar_width_overflow": max(0, fat / 0.7 - 100),
"unit": "g",
"color": "bg-fat",
"overflow_color": "bg-fat-dark",
},
]
return macros
user_bp.before_request(login_required) user_bp.before_request(login_required)
@@ -83,6 +31,35 @@ def dashboard():
return render_template("dashboard.html", items=items) return render_template("dashboard.html", items=items)
@user_bp.route("/daily_log", methods=["GET"])
def daily_log():
# Get today's date according to user's timezone
today = datetime.now(ZoneInfo(current_user.timezone)).date()
# Save date in session
session["selected_date"] = today.isoformat()
# Get logs from today
logs_today = current_user.food_logs.filter_by(
date_=today.isoformat()
).all()
# calculate macros
macros = array((0.0, 0.0, 0.0, 0.0))
for log in logs_today:
macros += array(log.food_item.macros()) / 100 * log.amount
macros = macro_arr_to_json(macros.tolist())
# Render HTML
return render_template(
"daily_log.html",
macros=macros,
logs=logs_today,
today=today.strftime("%d/%m/%Y"),
min=min,
)
@user_bp.route("/delete_food_item/<int:id>", methods=["POST"]) @user_bp.route("/delete_food_item/<int:id>", methods=["POST"])
def delete_food_item(id: int): def delete_food_item(id: int):
item = FoodItem.query.get(id) item = FoodItem.query.get(id)
@@ -119,35 +96,6 @@ def edit_food_item(id: int):
return redirect(url_for("user.dashboard")) return redirect(url_for("user.dashboard"))
@user_bp.route("/daily_log2", methods=["GET"])
def daily_log2():
# Get today's date according to user's timezone
today = datetime.now(ZoneInfo(current_user.timezone)).date()
# Save date in session
session["selected_date"] = today.isoformat()
# Get logs from today
logs_today = current_user.food_logs.filter_by(
date_=today.isoformat()
).all()
# calculate macros
macros = array((0.0, 0.0, 0.0, 0.0))
for log in logs_today:
macros += array(log.food_item.macros()) / 100 * log.amount
macros = macro_arr_to_json(macros.tolist())
# Render HTML
return render_template(
"daily_log2.html",
macros=macros,
logs=logs_today,
today=today.strftime("%d/%m/%Y"),
min=min,
)
@user_bp.route("/remove_log/<int:id>", methods=["POST"]) @user_bp.route("/remove_log/<int:id>", methods=["POST"])
def remove_log(id: int): def remove_log(id: int):
log = db.session.get(FoodLog, id) log = db.session.get(FoodLog, id)
@@ -158,4 +106,4 @@ def remove_log(id: int):
# Delete log # Delete log
db.session.delete(log) db.session.delete(log)
db.session.commit() db.session.commit()
return redirect(url_for("user.daily_log2")) return redirect(url_for("user.daily_log"))

View File

@@ -50,7 +50,7 @@
</a> </a>
<!-- Center Button (highlighted) --> <!-- Center Button (highlighted) -->
<a id="set_link_date" href="{{ url_for('add_meal_v2.get_barcode') }}" <a id="set_link_date" href="{{ url_for('add_meal.find_item') }}"
class="btn btn-success flex-fill mx-2 fw-bold rounded-pill"> class="btn btn-success flex-fill mx-2 fw-bold rounded-pill">
Add Item Add Item
</a> </a>

View File

@@ -14,7 +14,7 @@ def login_required():
def default_return(next_page: Optional[str] = None): def default_return(next_page: Optional[str] = None):
return redirect(url_for("user.daily_log2")) return redirect(url_for("user.daily_log"))
if next_page: if next_page:
return redirect(next_page) return redirect(next_page)
if current_user.is_admin: if current_user.is_admin:
@@ -29,3 +29,54 @@ def is_valid_timezone(tz: str) -> bool:
print(Exception) print(Exception)
return False return False
return True return True
def macro_arr_to_json(data: list[float]):
assert len(data) == 4
cal = data[0]
pro = data[3]
car = data[2]
fat = data[1]
macros = [
{
"name": "Calories",
"current": cal,
"target": 2000,
"bar_width": 100 - abs(cal / 20 - 100),
"bar_width_overflow": max(0, cal / 20 - 100),
"unit": " kcal",
"color": "bg-calories",
"overflow_color": "bg-calories-dark",
},
{
"name": "Protein",
"current": pro,
"target": 150,
"bar_width": 100 - abs(pro / 1.5 - 100),
"bar_width_overflow": max(0, pro / 1.5 - 100),
"unit": "g",
"color": "bg-protein",
"overflow_color": "bg-protein-dark",
},
{
"name": "Carbs",
"current": car,
"target": 250,
"bar_width": 100 - abs(car / 2.5 - 100),
"bar_width_overflow": max(0, car / 2.5 - 100),
"unit": "g",
"color": "bg-carbs",
"overflow_color": "bg-carbs-dark",
},
{
"name": "Fat",
"current": fat,
"target": 70,
"bar_width": 100 - abs(fat / 0.7 - 100),
"bar_width_overflow": max(0, fat / 0.7 - 100),
"unit": "g",
"color": "bg-fat",
"overflow_color": "bg-fat-dark",
},
]
return macros