Add food item management and improve barcode workflow

Implemented routes and forms for adding and viewing food items by barcode, including templates for displaying and entering nutritional information. Enhanced the scan workflow to redirect to food item details or entry form. Added admin ability to delete food items. Improved UI for login and scan pages. Updated FoodItem model and form fields for consistency and accuracy.
This commit is contained in:
2025-06-29 08:39:25 +02:00
parent 0919048cfd
commit a39f54dbb0
10 changed files with 260 additions and 90 deletions

View File

@@ -1,6 +1,7 @@
from flask import Blueprint, render_template, abort
from flask import Blueprint, render_template, abort, redirect, url_for
from flask_login import current_user
from models import FoodItem
from application import db
admin_bp = Blueprint(
"admin",
@@ -25,3 +26,12 @@ def food_items():
@admin_bp.route("/barcode_test", methods=["GET"])
def barcode_test():
return render_template("barcode_test.html")
@admin_bp.route("/delete_food/<int:id>", methods=["POST"])
def delete_food(id):
item = FoodItem.query.get(id)
if item:
db.session.delete(item)
db.session.commit()
return redirect(url_for("admin.food_items"))

View File

@@ -18,6 +18,7 @@ Food Nutritional Info
<th>Sugars (g)</th>
<th>Carbs (g)</th>
<th>Protein (g)</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@@ -30,6 +31,12 @@ Food Nutritional Info
<td>{{ food.sugar_100g }}</td>
<td>{{ food.carbs_100g }}</td>
<td>{{ food.protein_100g }}</td>
<td>
<form method="POST" action="{{ url_for('admin.delete_food', id=food.id) }}"
onsubmit="return confirm('Are you sure you want to delete this item?');">
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -0,0 +1,49 @@
{% extends "base.html" %}
{% block content %}
<form method="POST" action="{{ url_for('add_food_item') }}">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.barcode.label(class="form-label") }}
{{ form.barcode(class="form-control", readonly=true, style="background-color: #e9ecef; cursor: not-allowed;") }}
</div>
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
</div>
<div class="mb-3">
{{ form.energy.label(class="form-label") }}
{{ form.energy(class="form-control") }}
</div>
<div class="mb-3">
{{ form.protein.label(class="form-label") }}
{{ form.protein(class="form-control") }}
</div>
<div class="mb-3">
{{ form.carbs.label(class="form-label") }}
{{ form.carbs(class="form-control") }}
</div>
<div class="mb-3">
{{ form.sugar.label(class="form-label") }}
{{ form.sugar(class="form-control") }}
</div>
<div class="mb-3">
{{ form.fat.label(class="form-label") }}
{{ form.fat(class="form-control") }}
</div>
<div class="mb-3">
{{ form.saturated_fat.label(class="form-label") }}
{{ form.saturated_fat(class="form-control") }}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
{% endblock%}

View File

@@ -10,7 +10,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light p-4">
<body class="bg-light">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
<div class="container-fluid">
@@ -20,8 +20,14 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('scan') }}">Scan</a>
</li>
</ul>
<ul class="navbar-nav ms-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('logout') }}">Logout</a>
</li>

View File

@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block content %}
<div class="card mb-4" style="max-width: 600px;">
<div class="card-header">
<h5 class="card-title mb-0">{{ item.name }}</h5>
<small class="text-muted">Barcode: {{ item.barcode }}</small>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-sm-5">Energy per 100g</dt>
<dd class="col-sm-7">{{ item.energy_100g }} kcal</dd>
<dt class="col-sm-5">Protein per 100g</dt>
<dd class="col-sm-7">{{ "%.1f"|format(item.protein_100g) }} g</dd>
<dt class="col-sm-5">Carbohydrates per 100g</dt>
<dd class="col-sm-7">{{ "%.1f"|format(item.carbs_100g) }} g</dd>
<dt class="col-sm-5">Sugar per 100g</dt>
<dd class="col-sm-7">
{% if item.sugar_100g is not none %}
{{ "%.1f"|format(item.sugar_100g) }} g
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</dd>
<dt class="col-sm-5">Fats per 100g</dt>
<dd class="col-sm-7">{{ "%.1f"|format(item.fats_100g) }} g</dd>
<dt class="col-sm-5">Saturated fats per 100g</dt>
<dd class="col-sm-7">
{% if item.saturated_fats_100g is not none %}
{{ "%.1f"|format(item.saturated_fats_100g) }} g
{% else %}
<span class="text-muted">N/A</span>
{% endif %}
</dd>
</dl>
</div>
</div>
{% endblock%}

View File

@@ -1,10 +1,36 @@
{% extends "base.html" %}
{% block content %}
<form method="post">
{{form.hidden_tag()}}
{{form.username()}}
{{form.password()}}
{{form.submit()}}
</form>
<div class="container d-flex justify-content-center align-items-center">
<div class="card shadow-sm p-4" style="width: 100%; max-width: 400px;">
<h3 class="mb-4 text-center">Login</h3>
<form method="post" novalidate>
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control", placeholder="Enter username") }}
{% if form.username.errors %}
<div class="text-danger small">
{{ form.username.errors[0] }}
</div>
{% endif %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control", placeholder="Enter password") }}
{% if form.password.errors %}
<div class="text-danger small">
{{ form.password.errors[0] }}
</div>
{% endif %}
</div>
<div class="d-grid">
{{ form.submit(class="btn btn-primary btn-lg") }}
</div>
</form>
</div>
</div>
{% endblock%}

View File

@@ -1,76 +1,32 @@
{% extends "base.html" %}
{% block content %}
<div id="main" class="container text-center">
<div class="container py-5">
<div class="text-center mb-4">
<h1 class="fw-bold">📷 ZXing Barcode Scanner</h1>
<p class="text-muted">Use your camera to scan barcodes in real time.</p>
</div>
<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>
<div class="d-flex justify-content-center">
<button id="startButton" class="btn btn-primary px-4">Start Scanning</button>
<button id="stopButton" class="btn btn-danger px-4 ms-3">Stop</button>
</div>
</div>
<template id="scan_result">
<div>
<h5>Result:</h5>
<p id="result" class="fw-bold text-success"></p>
</div>
<div id="product-info"></div>
</template>
<template id="template_Reader">
<h1 class="mb-4">📷 ZXing Barcode Scanner</h1>
<div class="mb-3">
<video id="video" class="border rounded shadow-sm" width="100%" style="max-width: 500px;"></video>
</div>
<div class="mb-3">
<button id="startButton" class="btn btn-primary">Start Scanning</button>
<button id="stopButton" class="btn btn-danger ms-2">Stop</button>
</div>
</template>
<script type="module">
import { BrowserMultiFormatReader } from 'https://cdn.jsdelivr.net/npm/@zxing/library@0.21.3/+esm';
const mainElement = document.getElementById('main');
const readerTemplate = document.getElementById('template_Reader')
const readerClone = readerTemplate.content.cloneNode(true);
const resultTemplate = document.getElementById('scan_result')
const resultClone = resultTemplate.content.cloneNode(true);
mainElement.appendChild(readerClone);
// constants
const codeReader = new BrowserMultiFormatReader();
const videoElement = document.getElementById('video');
async function fetchProductData(barcode) {
// Step 1: GET Data
const response = await fetch(`/nutri/${barcode}`);
// Step 2: Check if response wasn't ok
if (!response.ok) throw new Error('Network response was not ok');
// Step 3: Convert response to json and check if empty
const nutritionData = await response.json();
mainElement.innerHTML = '';
mainElement.appendChild(resultClone);
const resultElement = document.getElementById('result');
resultElement.textContent = barcode;
const container = document.getElementById('product-info');
if (Object.keys(nutritionData).length === 0) {
// No data, enter new data
}
else {
container.innerHTML = `
<h2>${nutritionData.name}</h2>
<p><strong>Barcode:</strong> ${nutritionData.barcode}</p>
<p><strong>Energy:</strong> ${nutritionData.energy_100g} kcal per 100g</p>
<p><strong>Carbs:</strong> ${nutritionData.carbs_100g} g</p>
<p><strong>Sugar:</strong> ${nutritionData.sugar_100g} g</p>
<p><strong>Fats:</strong> ${nutritionData.fats_100g} g</p>
<p><strong>Saturated Fats:</strong> ${nutritionData.saturated_fats_100g} g</p>
<p><strong>Protein:</strong> ${nutritionData.protein_100g} g</p>
`;
}
}
// Start scanning for barcode
document.getElementById('startButton').addEventListener('click', async () => {
console.log('[DEBUG] Start button clicked')
@@ -89,20 +45,14 @@
await codeReader.decodeFromVideoDevice(selectedDeviceId, videoElement, async (result, err) => {
if (result) {
const codeText = result.getText();
try {
await fetchProductData(codeText)
codeReader.reset();
} catch (error) {
console.error('Error fetching nutrition data:', error);
}
const baseURL = "{{url_for('food_item', barcode='0')}}"
window.location.href = baseURL.replace("0", encodeURIComponent(codeText))
}
});
});
document.getElementById('stopButton').addEventListener('click', () => {
codeReader.reset();
resultElement.textContent = '';
container.innerHTML = ""
});
</script>
{% endblock %}