mirror of
https://github.com/StefBuwalda/cal_counter.git
synced 2025-10-30 03:10:00 +00:00
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:
@@ -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"))
|
||||
|
||||
@@ -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>
|
||||
|
||||
49
application/templates/add_food_item.html
Normal file
49
application/templates/add_food_item.html
Normal 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%}
|
||||
@@ -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>
|
||||
|
||||
44
application/templates/food_item.html
Normal file
44
application/templates/food_item.html
Normal 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%}
|
||||
@@ -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%}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user