diff --git a/add_products.py b/add_products.py new file mode 100644 index 0000000..5b23ad3 --- /dev/null +++ b/add_products.py @@ -0,0 +1,31 @@ +# 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()) + +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.") + +dodane = 0 +with app.app_context(): + for name in produkty: + if not SuggestedProduct.query.filter_by(name=name).first(): + prod = SuggestedProduct(name=name) + db.session.add(prod) + dodane += 1 + db.session.commit() + +print(f'Dodano {dodane} produktów do bazy.') diff --git a/app.py b/app.py index e169907..f985c1e 100644 --- a/app.py +++ b/app.py @@ -44,6 +44,7 @@ PROTECTED_JS_FILES = { "expenses.js", "toggle_button.js", "user_management.js", + "mass_add.js" } os.makedirs(UPLOAD_FOLDER, exist_ok=True) @@ -595,6 +596,12 @@ def uploaded_file(filename): response.headers['Content-Type'] = mime return response +@app.route('/all_products') +@login_required +def all_products(): + suggestions = SuggestedProduct.query.order_by(SuggestedProduct.name).all() + return jsonify([s.name for s in suggestions]) + @app.route('/admin') @login_required @admin_required diff --git a/static/css/style.css b/static/css/style.css index 1fba13e..bff9285 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -192,4 +192,24 @@ input.form-control { opacity: 1; transform: translateY(0); } -} \ No newline at end of file +} + +#mass-add-list li.active { + background: #198754 !important; + color: #fff !important; + font-weight: bold; +} +#mass-add-list li { + transition: background 0.2s; +} +.quantity-input { + width: 60px; + background: #343a40; + color: #fff; + border: 1px solid #495057; + border-radius: 4px; + text-align: center; +} +.add-btn { + margin-left: 10px; +} diff --git a/static/js/mass_add.js b/static/js/mass_add.js new file mode 100644 index 0000000..50b849b --- /dev/null +++ b/static/js/mass_add.js @@ -0,0 +1,88 @@ +document.addEventListener('DOMContentLoaded', function () { + const modal = document.getElementById('massAddModal'); + const productList = document.getElementById('mass-add-list'); + let addedProducts = new Set(); + + document.querySelectorAll('#items li').forEach(li => { + if (li.dataset.name) addedProducts.add(li.dataset.name.toLowerCase()); + }); + + modal.addEventListener('show.bs.modal', async function () { + productList.innerHTML = '
  • Ładowanie...
  • '; + try { + const res = await fetch('/all_products'); + const suggestions = await res.json(); + productList.innerHTML = ''; + suggestions.forEach(name => { + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center bg-dark text-light'; + li.textContent = name; + + if (addedProducts.has(name.toLowerCase())) { + li.classList.add('active'); + li.innerHTML += ''; + } else { + // Pole do ilości + const qty = document.createElement('input'); + qty.type = 'number'; + qty.min = 1; + qty.value = 1; + qty.className = 'quantity-input ms-2'; + qty.title = 'Ilość'; + + // Przycisk dodania + const btn = document.createElement('button'); + btn.className = 'btn btn-sm btn-primary add-btn'; + btn.textContent = '+'; + btn.onclick = () => { + const quantity = parseInt(qty.value) || 1; + socket.emit('add_item', { list_id: LIST_ID, name: name, quantity: quantity }); + }; + + li.textContent = name; + li.appendChild(qty); + li.appendChild(btn); + } + productList.appendChild(li); + }); + } catch (err) { + productList.innerHTML = '
  • Błąd ładowania danych
  • '; + } + }); + + socket.on('item_added', data => { + document.querySelectorAll('#mass-add-list li').forEach(li => { + if (li.textContent.trim().startsWith(data.name) && !li.classList.contains('active')) { + li.classList.add('active'); + li.innerHTML = `${data.name} `; + li.onclick = null; + } + }); + + const itemsContainer = document.getElementById('items'); + if (!itemsContainer) return; + if (document.getElementById(`item-${data.id}`)) return; + + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center flex-wrap item-not-checked'; + li.id = `item-${data.id}`; + li.dataset.name = data.name; + + let quantityBadge = ''; + if (data.quantity && data.quantity > 1) { + quantityBadge = `${data.quantity}×`; + } + + li.innerHTML = ` +
    + + + ${quantityBadge} +
    + `; + itemsContainer.appendChild(li); + addedProducts.add(data.name.toLowerCase()); + }); +}); diff --git a/templates/list.html b/templates/list.html index efe99c8..b3c63eb 100644 --- a/templates/list.html +++ b/templates/list.html @@ -102,11 +102,21 @@ Lista: {{ list.title }} {% if not list.is_archived %} -
    - - - +
    +
    + +
    +
    +
    + + + +
    +
    + {% endif %} {% set receipt_pattern = 'list_' ~ list.id %} @@ -127,7 +137,26 @@ Lista: {{ list.title }}

    Brak wgranych paragonów do tej listy.

    {% endif %} + + {% block scripts %} +