From 1561ea1ab622f3b7caaf991bef2e5ebca0933309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Wed, 9 Jul 2025 10:22:35 +0200 Subject: [PATCH] ux i funkcja masowego dodwania produktu --- alters.txt | 4 ++ app.py | 41 ++++++++++++--- static/js/mass_add.js | 116 +++++++++++++++++++++++++++--------------- templates/list.html | 2 +- 4 files changed, 113 insertions(+), 50 deletions(-) diff --git a/alters.txt b/alters.txt index 452566e..fffe528 100644 --- a/alters.txt +++ b/alters.txt @@ -27,3 +27,7 @@ ALTER TABLE shopping_list ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT 1; # ilośc produktów ALTER TABLE item ADD COLUMN quantity INTEGER DEFAULT 1; + +#licznik najczesciej kupowanych reczy +ALTER TABLE suggested_product ADD COLUMN usage_count INTEGER DEFAULT 0; + diff --git a/app.py b/app.py index f985c1e..bf6d269 100644 --- a/app.py +++ b/app.py @@ -98,7 +98,7 @@ class Item(db.Model): class SuggestedProduct(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(150), unique=True, nullable=False) - + usage_count = db.Column(db.Integer, default=0) class Expense(db.Model): id = db.Column(db.Integer, primary_key=True) list_id = db.Column(db.Integer, db.ForeignKey('shopping_list.id')) @@ -561,6 +561,39 @@ def suggest_products(): suggestions = SuggestedProduct.query.filter(SuggestedProduct.name.ilike(f'%{query}%')).limit(5).all() return {'suggestions': [s.name for s in suggestions]} +@app.route('/all_products') +def all_products(): + query = request.args.get('q', '') + + top_products_query = SuggestedProduct.query + if query: + top_products_query = top_products_query.filter(SuggestedProduct.name.ilike(f'%{query}%')) + top_products = ( + top_products_query + .order_by(SuggestedProduct.usage_count.desc(), SuggestedProduct.name.asc()) + .limit(20) + .all() + ) + + top_names = [s.name for s in top_products] + rest_query = SuggestedProduct.query + if query: + rest_query = rest_query.filter(SuggestedProduct.name.ilike(f'%{query}%')) + + if top_names: + rest_query = rest_query.filter(~SuggestedProduct.name.in_(top_names)) + + rest_products = ( + rest_query + .order_by(SuggestedProduct.name.asc()) + .limit(200) + .all() + ) + + all_names = top_names + [s.name for s in rest_products] + + return {'allproducts': all_names} + @app.route('/upload_receipt/', methods=['POST']) def upload_receipt(list_id): if 'receipt' not in request.files: @@ -596,12 +629,6 @@ 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/js/mass_add.js b/static/js/mass_add.js index 50b849b..afdeee1 100644 --- a/static/js/mass_add.js +++ b/static/js/mass_add.js @@ -1,38 +1,77 @@ 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 () { + // 🔥 Za każdym razem od nowa budujemy zbiór produktów już na liście + let addedProducts = new Set(); + document.querySelectorAll('#items li').forEach(li => { + if (li.dataset.name) { + addedProducts.add(li.dataset.name.toLowerCase()); + } + }); + productList.innerHTML = '
  • Ładowanie...
  • '; try { const res = await fetch('/all_products'); - const suggestions = await res.json(); + const data = await res.json(); + const allproducts = data.allproducts; productList.innerHTML = ''; - suggestions.forEach(name => { + allproducts.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 += ''; + // Produkt już dodany — oznacz jako nieaktywny + li.classList.add('opacity-50'); + const badge = document.createElement('span'); + badge.className = 'badge bg-success ms-auto'; + badge.textContent = 'Dodano'; + li.appendChild(badge); } else { - // Pole do ilości + // Kontener na minus, pole i plus + const qtyWrapper = document.createElement('div'); + qtyWrapper.className = 'd-flex align-items-center ms-2'; + + // Minus + const minusBtn = document.createElement('button'); + minusBtn.type = 'button'; + minusBtn.className = 'btn btn-outline-light btn-sm px-2'; + minusBtn.textContent = '−'; + minusBtn.onclick = () => { + qty.value = Math.max(1, parseInt(qty.value) - 1); + }; + + // Pole ilości const qty = document.createElement('input'); qty.type = 'number'; qty.min = 1; qty.value = 1; - qty.className = 'quantity-input ms-2'; + qty.className = 'form-control text-center p-1'; + qty.classList.add('rounded'); + qty.style.width = '50px'; + qty.style.flex = '0 0 auto'; + qty.style.margin = '0 2px'; qty.title = 'Ilość'; + // Plus + const plusBtn = document.createElement('button'); + plusBtn.type = 'button'; + plusBtn.className = 'btn btn-outline-light btn-sm px-2'; + plusBtn.textContent = '+'; + plusBtn.onclick = () => { + qty.value = parseInt(qty.value) + 1; + }; + + // Dodajemy przyciski i input do wrappera + qtyWrapper.appendChild(minusBtn); + qtyWrapper.appendChild(qty); + qtyWrapper.appendChild(plusBtn); + // Przycisk dodania const btn = document.createElement('button'); - btn.className = 'btn btn-sm btn-primary add-btn'; + btn.className = 'btn btn-sm btn-primary ms-2'; btn.textContent = '+'; btn.onclick = () => { const quantity = parseInt(qty.value) || 1; @@ -40,7 +79,7 @@ document.addEventListener('DOMContentLoaded', function () { }; li.textContent = name; - li.appendChild(qty); + li.appendChild(qtyWrapper); li.appendChild(btn); } productList.appendChild(li); @@ -50,39 +89,32 @@ document.addEventListener('DOMContentLoaded', function () { } }); + // 🔥 Aktualizacja na żywo po dodaniu 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} `; + const itemName = li.firstChild.textContent.trim(); + + if (itemName === data.name && !li.classList.contains('opacity-50')) { + // Usuń wszystkie dzieci + while (li.firstChild) { + li.removeChild(li.firstChild); + } + + // Ustaw nazwę + li.textContent = data.name; + + // Dodaj klasę wyszarzenia + li.classList.add('opacity-50'); + + // Dodaj badge + const badge = document.createElement('span'); + badge.className = 'badge bg-success ms-auto'; + badge.textContent = 'Dodano'; + li.appendChild(badge); + + // Zablokuj kliknięcia 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 b3c63eb..0b71746 100644 --- a/templates/list.html +++ b/templates/list.html @@ -74,7 +74,7 @@ Lista: {{ list.title }}