zmiany w produktach i sugestoach / panel

This commit is contained in:
Mateusz Gruszczyński
2025-07-10 17:23:18 +02:00
parent 59d505d955
commit 29bcc304b7
4 changed files with 123 additions and 59 deletions

View File

@@ -1,31 +1,87 @@
# add_products_from_openfoodfacts.py
# Uruchom w katalogu z app.py
import urllib.request
import json
from app import db, SuggestedProduct, app
# URL do pobrania polskich produktów (tylko nazwy)
url = 'https://pl.openfoodfacts.org/cgi/search.pl?search_simple=1&action=process&json=1&fields=product_name&page_size=1000'
with urllib.request.urlopen(url) as response:
data = json.loads(response.read())
CATEGORIES = {
"Przyprawa": [
"przyprawa", "pieprz", "sól", "bazylia", "oregano", "papryka", "majeranek", "czosnek",
"tymianek", "rozmaryn", "kolendra", "curry", "imbir", "goździki", "chili", "koper",
"kminek", "liść laurowy", "ziele angielskie", "kurkuma", "musztarda", "chrzan"
],
"Mięso": [
"kurczak", "piersi z kurczaka", "udka z kurczaka", "wołowina", "mielona wołowina",
"wieprzowina", "schab", "łopatka", "szynka", "boczek", "indyk", "filet z indyka",
"gulasz", "pasztet", "karkówka", "żeberka", "kiełbasa", "parówki", "salami", "kabanos"
],
"Ryba i owoce morza": [
"łosoś", "dorsz", "mintaj", "makrela", "pstrąg", "karp", "śledź", "tuńczyk",
"morszczuk", "sardynka", "szproty", "anchois", "tilapia", "sandacz", "halibut",
"sum", "flądra", "ostrobok", "paluszki rybne", "konserwa rybna"
],
"Nabiał": [
"mleko", "jogurt", "ser żółty", "ser biały", "twaróg", "śmietana", "masło",
"kefir", "maślanka", "serek wiejski", "serek topiony", "mozzarella", "feta",
"parmezan", "gouda", "emmental", "ser pleśniowy", "ser homogenizowany",
"serek mascarpone", "ser ricotta"
],
"Warzywo": [
"pomidor", "ogórek", "marchew", "cebula", "sałata", "papryka", "ziemniak",
"kapusta", "brokuł", "kalafior", "cukinia", "bakłażan", "szpinak", "rukola",
"seler", "por", "burak", "dynia", "rzodkiewka", "fasola"
],
"Owoc": [
"jabłko", "banan", "gruszka", "truskawka", "winogrono", "malina", "borówka",
"czereśnia", "wiśnia", "brzoskwinia", "nektaryna", "śliwka", "ananas",
"mango", "kiwi", "cytryna", "limonka", "pomarańcza", "mandarynka", "grejpfrut"
],
"Pieczywo i zboża": [
"chleb", "bułka", "bagietka", "kajzerka", "pumpernikiel", "chleb razowy",
"chleb żytni", "tost", "grahamka", "croissant", "tortilla", "pizza",
"pierogi", "ryż", "makaron", "kasza jaglana", "kasza gryczana", "owsianka",
"płatki kukurydziane", "musli"
],
"Słodycze i przekąski": [
"czekolada", "baton", "ciastko", "wafel", "lody", "cukierek", "żelki",
"herbatnik", "paluszki", "chipsy", "orzeszki", "popcorn", "krakersy",
"ciasto", "muffin", "pączek", "drożdżówka", "babeczka", "piernik", "beza"
],
"Napoje": [
"woda", "sok jabłkowy", "sok pomarańczowy", "sok multiwitamina", "cola",
"pepsi", "napój gazowany", "kawa", "herbata", "piwo", "wino czerwone",
"wino białe", "tonik", "lemoniada", "napój izotoniczny", "kompot",
"napój mleczny", "maślanka pitna", "koktajl owocowy", "nektar"
],
"Tłuszcze i oleje": [
"oliwa", "olej rzepakowy", "olej słonecznikowy", "masło klarowane",
"margaryna", "smalec", "masło orzechowe", "tłuszcz kokosowy",
"olej lniany", "olej z pestek winogron", "olej sezamowy",
"olej ryżowy", "olej z awokado", "olej kukurydziany", "olej arachidowy",
"olej palmowy", "olej konopny", "olej sojowy", "olej dyniowy", "olej z orzechów włoskich"
],
"Dania gotowe": [
"pizza", "hamburger", "hot dog", "zupa", "gulasz", "pierogi ruskie",
"pierogi z mięsem", "lasagne", "sałatka warzywna", "kanapka",
"wrap", "tortilla", "zapiekanka", "sushi", "falafel", "kebab",
"pyzy", "kluski śląskie", "kotlet schabowy", "gołąbki"
]
}
produkty = []
for prod in data.get('products', []):
name = prod.get('product_name', '').strip()
if name:
produkty.append(name)
print(f"Znaleziono {len(produkty)} produktów do dodania.")
for category, names in CATEGORIES.items():
for name in names:
produkty.append((category, name.lower().strip()))
print(f"Przygotowano {len(produkty)} produktów do dodania.")
dodane = 0
with app.app_context():
for name in produkty:
if not SuggestedProduct.query.filter_by(name=name).first():
prod = SuggestedProduct(name=name)
dodane = 0
for category, name in produkty:
full_name = f"{category}: {name}"
if not SuggestedProduct.query.filter_by(name=full_name).first():
prod = SuggestedProduct(name=full_name)
db.session.add(prod)
dodane += 1
db.session.commit()
print(f'Dodano {dodane} produktów do bazy.')
print(f"Dodano {dodane} produktów do bazy.")

5
app.py
View File

@@ -945,8 +945,8 @@ def list_products():
users = User.query.all()
users_dict = {user.id: user.username for user in users}
# Wszystkie sugestie do słownika
suggestions = SuggestedProduct.query.all()
# Stabilne sortowanie sugestii
suggestions = SuggestedProduct.query.order_by(SuggestedProduct.name.asc()).all()
suggestions_dict = {s.name.lower(): s for s in suggestions}
return render_template(
@@ -956,6 +956,7 @@ def list_products():
suggestions_dict=suggestions_dict
)
@app.route('/admin/sync_suggestion/<int:item_id>', methods=['POST'])
@login_required
def sync_suggestion_ajax(item_id):

View File

@@ -1,5 +1,5 @@
document.addEventListener("DOMContentLoaded", function() {
// Usuń stare eventy
// Odśwież eventy
document.querySelectorAll('.sync-btn').forEach(btn => {
btn.replaceWith(btn.cloneNode(true));
});
@@ -8,14 +8,12 @@ document.addEventListener("DOMContentLoaded", function() {
});
// Synchronizacja sugestii
const buttons = document.querySelectorAll('.sync-btn');
buttons.forEach(btn => {
document.querySelectorAll('.sync-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const itemId = this.getAttribute('data-item-id');
const button = this;
button.disabled = true;
fetch(`/admin/sync_suggestion/${itemId}`, {
@@ -36,7 +34,7 @@ document.addEventListener("DOMContentLoaded", function() {
button.disabled = false;
}
})
.catch(error => {
.catch(() => {
showToast('Błąd synchronizacji', 'danger');
button.disabled = false;
});
@@ -44,14 +42,12 @@ document.addEventListener("DOMContentLoaded", function() {
});
// Usuwanie sugestii
const deleteButtons = document.querySelectorAll('.delete-suggestion-btn');
deleteButtons.forEach(btn => {
document.querySelectorAll('.delete-suggestion-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const suggestionId = this.getAttribute('data-suggestion-id');
const button = this;
button.disabled = true;
fetch(`/admin/delete_suggestion/${suggestionId}`, {
@@ -65,41 +61,16 @@ document.addEventListener("DOMContentLoaded", function() {
showToast(data.message, data.success ? 'success' : 'danger');
if (data.success) {
// Możesz usunąć cały wiersz lub np. odświeżyć kolumnę
const row = button.closest('tr');
if (row) {
row.remove();
}
if (row) row.remove();
} else {
button.disabled = false;
}
})
.catch(error => {
.catch(() => {
showToast('Błąd usuwania sugestii', 'danger');
button.disabled = false;
});
});
});
});
// Funkcja do wyświetlania toast
function showToast(message, type = 'success') {
const toastContainer = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type} border-0 show`;
toast.role = 'alert';
toast.style.minWidth = '250px';
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">${message}</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
toastContainer.appendChild(toast);
// Automatyczne usunięcie po 1.75 sekundy
setTimeout(() => {
toast.remove();
}, 1750);
}

View File

@@ -7,7 +7,7 @@
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
</div>
<div class="card bg-dark text-white">
<div class="card bg-dark text-white mb-5">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="m-0">📦 Produkty (z synchronizacją sugestii)</h4>
<span class="badge bg-secondary">{{ items|length }} produktów</span>
@@ -39,8 +39,7 @@
{% set suggestion = suggestions_dict.get(item.name.lower()) %}
{% if suggestion %}
✅ Istnieje (ID: {{ suggestion.id }})
<button class="btn btn-sm btn-outline-danger ms-1 delete-suggestion-btn" data-suggestion-id="{{ suggestion.id }}">🗑️ Usuń</button>
<button class="btn btn-sm btn-outline-danger ms-1 delete-suggestion-btn" data-suggestion-id="{{ suggestion.id }}">🗑️ Usuń</button>
{% else %}
<button class="btn btn-sm btn-outline-primary sync-btn" data-item-id="{{ item.id }}">🔄 Synchronizuj</button>
{% endif %}
@@ -60,8 +59,45 @@
</div>
</div>
{% block scripts %}
<!-- Tabela z samymi sugestiami -->
<div class="card bg-dark text-white">
<div class="card-header d-flex justify-content-between align-items-center">
<h4 class="m-0">💡 Wszystkie sugestie (poza powiązanymi)</h4>
<span class="badge bg-secondary">{{ suggestions_dict|length }} sugestii</span>
</div>
<div class="card-body p-0">
{% set item_names = items | map(attribute='name') | map('lower') | list %}
<table class="table table-dark table-striped align-middle m-0">
<thead>
<tr>
<th>ID</th>
<th>Nazwa</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for suggestion in suggestions_dict.values() %}
{% if suggestion.name.lower() not in item_names %}
<tr>
<td>{{ suggestion.id }}</td>
<td class="fw-bold">{{ suggestion.name }}</td>
<td>
<button class="btn btn-sm btn-outline-danger delete-suggestion-btn" data-suggestion-id="{{ suggestion.id }}">🗑️ Usuń</button>
</td>
</tr>
{% endif %}
{% endfor %}
{% if suggestions_dict|length == 0 %}
<tr>
<td colspan="3" class="text-center text-muted">Brak sugestii do wyświetlenia.</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='product_suggestion.js') }}"></script>
{% endblock %}