From 529130a622b68dfaa1bdc8e7357cb592a4dad611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 12:22:22 +0200 Subject: [PATCH] sortowanie_w_mass_add --- app.py | 54 +++++----- static/js/mass_add.js | 245 ++++++++++++++++++++++++++---------------- templates/list.html | 11 +- 3 files changed, 188 insertions(+), 122 deletions(-) diff --git a/app.py b/app.py index 6f28d09..109d675 100644 --- a/app.py +++ b/app.py @@ -1952,46 +1952,46 @@ def suggest_products(): @app.route("/all_products") def all_products(): query = request.args.get("q", "") + sort = request.args.get("sort", "popularity") + limit = request.args.get("limit", type=int) or 50 + offset = request.args.get("offset", type=int) or 0 top_products_query = SuggestedProduct.query if query: top_products_query = top_products_query.filter( SuggestedProduct.name.ilike(f"%{query}%") ) - - top_products = ( + + # Liczenie popularności + popularity_subquery = ( db.session.query( - func.lower(Item.name).label("name"), func.sum(Item.quantity).label("count") + func.lower(Item.name).label("name"), + func.sum(Item.quantity).label("count") ) - .join(ShoppingList, ShoppingList.id == Item.list_id) - .filter(Item.purchased.is_(True)) .group_by(func.lower(Item.name)) - .order_by(func.sum(Item.quantity).desc()) - .limit(5) - .all() + .subquery() ) - 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}%")) + query_base = db.session.query( + SuggestedProduct.name, + popularity_subquery.c.count + ).outerjoin( + popularity_subquery, func.lower(SuggestedProduct.name) == popularity_subquery.c.name + ) - if top_names: - rest_query = rest_query.filter(~SuggestedProduct.name.in_(top_names)) + if sort == "alphabetical": + query_base = query_base.order_by(func.lower(SuggestedProduct.name).asc(), popularity_subquery.c.count.desc()) + else: # popularity + query_base = query_base.order_by(popularity_subquery.c.count.desc().nullslast(), func.lower(SuggestedProduct.name).asc()) + + products = query_base.offset(offset).limit(limit).all() + products_list = [ + {"name": name, "count": count or 0} + for name, count in products + ] + + return jsonify({"products": products_list, "limit": limit}) - rest_products = rest_query.order_by(SuggestedProduct.name.asc()).limit(200).all() - - all_names = top_names + [s.name for s in rest_products] - - seen = set() - unique_names = [] - for name in all_names: - name_lower = name.strip().lower() - if name_lower not in seen: - unique_names.append(name) - seen.add(name_lower) - - return {"allproducts": unique_names} @app.route("/upload_receipt/", methods=["POST"]) diff --git a/static/js/mass_add.js b/static/js/mass_add.js index b573fb1..6c8706f 100644 --- a/static/js/mass_add.js +++ b/static/js/mass_add.js @@ -1,116 +1,184 @@ document.addEventListener('DOMContentLoaded', function () { const modal = document.getElementById('massAddModal'); const productList = document.getElementById('mass-add-list'); + const sortBar = document.getElementById('sort-bar'); + const productCountDisplay = document.getElementById('product-count'); - // Funkcja normalizacji (usuwa diakrytyki i zamienia na lowercase) + // Normalizacja nazw function normalize(str) { - return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase(); + return str ? str.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase() : ''; } - modal.addEventListener('show.bs.modal', async function () { - let addedProducts = new Set(); - document.querySelectorAll('#items li').forEach(li => { - if (li.dataset.name) { - addedProducts.add(normalize(li.dataset.name)); + let sortMode = 'popularity'; // 'popularity' lub 'alphabetical' + let limit = 50; + let offset = 0; + let loading = false; + let reachedEnd = false; + let allProducts = []; + let addedProducts = new Set(); + + function renderSortBar() { + if (!sortBar) return; + sortBar.innerHTML = ` + Sortuj: Popularność | + Alfabetycznie + `; + document.getElementById('sort-popularity').onclick = (e) => { + e.preventDefault(); + if (sortMode !== 'popularity') { + sortMode = 'popularity'; + resetAndFetchProducts(); } - }); + }; + document.getElementById('sort-alphabetical').onclick = (e) => { + e.preventDefault(); + if (sortMode !== 'alphabetical') { + sortMode = 'alphabetical'; + resetAndFetchProducts(); + } + }; + } - productList.innerHTML = '
  • Ładowanie...
  • '; + function resetAndFetchProducts() { + offset = 0; + reachedEnd = false; + allProducts = []; + productList.innerHTML = ''; + fetchProducts(true); + renderSortBar(); + if (productCountDisplay) productCountDisplay.textContent = ''; + } + + async function fetchProducts(reset = false) { + if (loading || reachedEnd) return; + loading = true; try { - const res = await fetch('/all_products'); + productList.innerHTML = '
  • Ładowanie...
  • '; + const res = await fetch(`/all_products?sort=${sortMode}&limit=${limit}&offset=${offset}`); const data = await res.json(); - const allproducts = data.allproducts; - productList.innerHTML = ''; - 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'; - - if (addedProducts.has(normalize(name))) { - const nameSpan = document.createElement('span'); - nameSpan.textContent = name; - li.appendChild(nameSpan); - - li.classList.add('opacity-50'); - const badge = document.createElement('span'); - badge.className = 'badge bg-success ms-auto'; - badge.textContent = 'Dodano'; - li.appendChild(badge); - } else { - const nameSpan = document.createElement('span'); - nameSpan.textContent = name; - nameSpan.style.flex = '1 1 auto'; - li.appendChild(nameSpan); - - const qtyWrapper = document.createElement('div'); - qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls'; - - 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); - }; - - const qty = document.createElement('input'); - qty.type = 'number'; - qty.min = 1; - qty.value = 1; - qty.className = 'form-control text-center p-1 rounded'; - qty.style.width = '50px'; - qty.style.margin = '0 2px'; - qty.title = 'Ilość'; - - 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; - }; - - qtyWrapper.appendChild(minusBtn); - qtyWrapper.appendChild(qty); - qtyWrapper.appendChild(plusBtn); - - const btn = document.createElement('button'); - btn.className = 'btn btn-sm btn-primary ms-4'; - btn.textContent = '+'; - - btn.onclick = () => { - const quantity = parseInt(qty.value) || 1; - socket.emit('add_item', { list_id: LIST_ID, name: name, quantity: quantity }); - }; - - li.appendChild(qtyWrapper); - li.appendChild(btn); - } - productList.appendChild(li); - }); - + const products = data.products || data.allproducts || []; + if (products.length < limit) reachedEnd = true; + allProducts = reset ? products : allProducts.concat(products); + renderProducts(products, reset); + offset += limit; + if (productCountDisplay) { + productCountDisplay.textContent = `Wyświetlono ${allProducts.length} produktów.`; + } } catch (err) { productList.innerHTML = '
  • Błąd ładowania danych
  • '; } + loading = false; + } + + function getAlreadyAddedProducts() { + const set = new Set(); + document.querySelectorAll('#items li').forEach(li => { + if (li.dataset.name) { + set.add(normalize(li.dataset.name)); + } + }); + return set; + } + + function renderProducts(products, reset) { + if (reset) productList.innerHTML = ''; + addedProducts = getAlreadyAddedProducts(); + products.forEach(name => { + if (typeof name === "object" && name.name) name = name.name; + const li = document.createElement('li'); + li.className = 'list-group-item d-flex justify-content-between align-items-center bg-dark text-light'; + + if (addedProducts.has(normalize(name))) { + const nameSpan = document.createElement('span'); + nameSpan.textContent = name; + li.appendChild(nameSpan); + li.classList.add('opacity-50'); + const badge = document.createElement('span'); + badge.className = 'badge bg-success ms-auto'; + badge.textContent = 'Dodano'; + li.appendChild(badge); + } else { + const nameSpan = document.createElement('span'); + nameSpan.textContent = name; + nameSpan.style.flex = '1 1 auto'; + li.appendChild(nameSpan); + + const qtyWrapper = document.createElement('div'); + qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls'; + + const minusBtn = document.createElement('button'); + minusBtn.type = 'button'; + minusBtn.className = 'btn btn-outline-light btn-sm px-2'; + minusBtn.textContent = '−'; + + const qty = document.createElement('input'); + qty.type = 'number'; + qty.min = 1; + qty.value = 1; + qty.className = 'form-control text-center p-1 rounded'; + qty.style.width = '50px'; + qty.style.margin = '0 2px'; + qty.title = 'Ilość'; + + const plusBtn = document.createElement('button'); + plusBtn.type = 'button'; + plusBtn.className = 'btn btn-outline-light btn-sm px-2'; + plusBtn.textContent = '+'; + + minusBtn.onclick = () => { + qty.value = Math.max(1, parseInt(qty.value) - 1); + }; + plusBtn.onclick = () => { + qty.value = parseInt(qty.value) + 1; + }; + + qtyWrapper.appendChild(minusBtn); + qtyWrapper.appendChild(qty); + qtyWrapper.appendChild(plusBtn); + + const btn = document.createElement('button'); + btn.className = 'btn btn-sm btn-primary ms-4'; + btn.textContent = '+'; + btn.onclick = () => { + const quantity = parseInt(qty.value) || 1; + socket.emit('add_item', { list_id: LIST_ID, name: name, quantity: quantity }); + }; + + li.appendChild(qtyWrapper); + li.appendChild(btn); + } + productList.appendChild(li); + }); + } + + // Infinite scroll + modal.addEventListener('scroll', function () { + if (!loading && !reachedEnd && (modal.scrollTop + modal.offsetHeight > modal.scrollHeight - 80)) { + fetchProducts(false); + } }); + // Modal otwarty + modal.addEventListener('show.bs.modal', function () { + resetAndFetchProducts(); + }); + + renderSortBar(); + + // Obsługa dodania przez socket socket.on('item_added', data => { document.querySelectorAll('#mass-add-list li').forEach(li => { const itemName = li.firstChild?.textContent.trim(); - if (normalize(itemName) === normalize(data.name) && !li.classList.contains('opacity-50')) { li.classList.add('opacity-50'); - - // Usuń poprzednie przyciski li.querySelectorAll('button').forEach(btn => btn.remove()); const quantityControls = li.querySelector('.quantity-controls'); if (quantityControls) quantityControls.remove(); - // Badge "Dodano" const badge = document.createElement('span'); badge.className = 'badge bg-success'; badge.textContent = 'Dodano'; - // Grupowanie przycisku + licznika const btnGroup = document.createElement('div'); btnGroup.className = 'btn-group btn-group-sm me-2'; btnGroup.role = 'group'; @@ -127,14 +195,12 @@ document.addEventListener('DOMContentLoaded', function () { btnGroup.appendChild(undoBtn); btnGroup.appendChild(timerBtn); - // Kontener na prawą stronę const rightWrapper = document.createElement('div'); rightWrapper.className = 'd-flex align-items-center gap-2 ms-auto'; rightWrapper.appendChild(btnGroup); rightWrapper.appendChild(badge); li.appendChild(rightWrapper); - // Odliczanie const intervalId = setInterval(() => { secondsLeft--; if (secondsLeft > 0) { @@ -145,14 +211,11 @@ document.addEventListener('DOMContentLoaded', function () { } }, 1000); - // Obsługa cofnięcia undoBtn.onclick = () => { clearInterval(intervalId); btnGroup.remove(); badge.remove(); li.classList.remove('opacity-50'); - - // Przywróć kontrolki ilości const qtyWrapper = document.createElement('div'); qtyWrapper.className = 'd-flex align-items-center ms-2 quantity-controls'; @@ -185,7 +248,6 @@ document.addEventListener('DOMContentLoaded', function () { qtyWrapper.append(minusBtn, qty, plusBtn); li.appendChild(qtyWrapper); - // Dodaj przycisk dodawania const addBtn = document.createElement('button'); addBtn.className = 'btn btn-sm btn-primary ms-4'; addBtn.textContent = '+'; @@ -199,13 +261,10 @@ document.addEventListener('DOMContentLoaded', function () { }; li.appendChild(addBtn); - // Usuń z listy socket.emit('delete_item', { item_id: data.id }); }; } }); }); - }); - diff --git a/templates/list.html b/templates/list.html index 902448f..4919ed5 100644 --- a/templates/list.html +++ b/templates/list.html @@ -227,8 +227,14 @@ + {% block scripts %}