From b61c2621792269ca12694c6509d9911ec46ad968 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Fri, 15 Aug 2025 10:01:05 +0200 Subject: [PATCH] paginacja i poprawki uxowe --- app.py | 103 +++++++++++++--------- static/js/preview_list_modal.js | 62 +++++++++---- templates/admin/edit_list.html | 2 +- templates/admin/list_products.html | 34 ++++++- templates/admin/mass_edit_categories.html | 31 +++++++ templates/admin/receipts.html | 56 ++++++------ templates/admin/user_management.html | 2 +- 7 files changed, 200 insertions(+), 90 deletions(-) diff --git a/app.py b/app.py index 852e35e..a58a990 100644 --- a/app.py +++ b/app.py @@ -2447,21 +2447,21 @@ def delete_user(user_id): @admin_required def admin_receipts(id): try: + page = request.args.get("page", 1, type=int) + per_page = request.args.get("per_page", 24, type=int) + per_page = max(1, min(per_page, 200)) # sanity check + if id == "all": all_filenames = {r.filename for r in Receipt.query.all()} - per_page = 24 - page = request.args.get("page", 1, type=int) - total_count = Receipt.query.count() - total_pages = (total_count + per_page - 1) // per_page - - receipts_paginated = ( + pagination = ( Receipt.query.order_by(Receipt.uploaded_at.desc()) - .offset((page - 1) * per_page) - .limit(per_page) - .all() + .paginate(page=page, per_page=per_page, error_out=False) ) + receipts_paginated = pagination.items + total_pages = pagination.pages + upload_folder = app.config["UPLOAD_FOLDER"] files_on_disk = set(os.listdir(upload_folder)) orphan_files = [ @@ -2471,7 +2471,6 @@ def admin_receipts(id): and f not in all_filenames and f.startswith("list_") ] - else: list_id = int(id) receipts_paginated = ( @@ -2482,13 +2481,12 @@ def admin_receipts(id): orphan_files = [] page = 1 total_pages = 1 + per_page = len(receipts_paginated) or 1 except ValueError: flash("Nieprawidłowe ID listy.", "danger") return redirect(url_for("admin_panel")) - args_without_page = request.args.to_dict() - args_without_page.pop("page", None) - query_string = urlencode(args_without_page) + query_string = urlencode({k: v for k, v in request.args.items() if k != "page"}) return render_template( "admin/receipts.html", @@ -2496,6 +2494,7 @@ def admin_receipts(id): orphan_files=orphan_files, orphan_files_count=len(orphan_files), page=page, + per_page=per_page, total_pages=total_pages, id=id, query_string=query_string, @@ -2834,10 +2833,17 @@ def edit_list(list_id): @login_required @admin_required def list_products(): - items = Item.query.options( + page = request.args.get("page", 1, type=int) + per_page = request.args.get("per_page", 125, type=int) + per_page = max(1, min(per_page, 300)) + + items_query = Item.query.options( joinedload(Item.shopping_list), joinedload(Item.added_by_user), - ).order_by(Item.id.desc()).all() + ).order_by(Item.id.desc()) + + pagination = items_query.paginate(page=page, per_page=per_page, error_out=False) + items = pagination.items users = User.query.all() users_dict = {user.id: user.username for user in users} @@ -2847,19 +2853,7 @@ def list_products(): (s.name or "").strip().lower(): s for s in suggestions if s.name and s.name.strip() } - seen_names = set() - unique_items = [] - used_suggestion_names = set() - - for item in items: - normalized_name = (item.name or "").strip().lower() - if not normalized_name: - continue - if normalized_name not in seen_names: - seen_names.add(normalized_name) - unique_items.append(item) - used_suggestion_names.add(normalized_name) - + used_suggestion_names = {(item.name or "").strip().lower() for item in items if item.name} orphan_suggestions = [ s for s in suggestions if (s.name or "").strip().lower() not in used_suggestion_names and (s.name or "").strip() @@ -2871,12 +2865,18 @@ def list_products(): if name in all_suggestions_dict } + query_string = urlencode({k: v for k, v in request.args.items() if k != "page"}) + return render_template( "admin/list_products.html", - items=unique_items, + items=items, users_dict=users_dict, suggestions_dict=suggestions_dict, orphan_suggestions=orphan_suggestions, + page=page, + per_page=per_page, + total_pages=pagination.pages, + query_string=query_string, ) @@ -2981,19 +2981,23 @@ def recalculate_filesizes_all(): return redirect(url_for("admin_receipts", id="all")) + @app.route("/admin/mass_edit_categories", methods=["GET", "POST"]) @login_required @admin_required def admin_mass_edit_categories(): - lists = ( - ShoppingList.query.options( - joinedload(ShoppingList.categories), - joinedload(ShoppingList.items), - joinedload(ShoppingList.owner), - ) - .order_by(ShoppingList.created_at.desc()) - .all() - ) + page = request.args.get("page", 1, type=int) + per_page = request.args.get("per_page", 50, type=int) + per_page = max(1, min(per_page, 200)) # ogranicz do sensownych wartości + + lists_query = ShoppingList.query.options( + joinedload(ShoppingList.categories), + joinedload(ShoppingList.items), + joinedload(ShoppingList.owner), + ).order_by(ShoppingList.created_at.desc()) + + pagination = lists_query.paginate(page=page, per_page=per_page, error_out=False) + lists = pagination.items categories = Category.query.order_by(Category.name.asc()).all() @@ -3011,12 +3015,19 @@ def admin_mass_edit_categories(): l.categories.extend(cats) db.session.commit() flash("Zaktualizowano kategorie dla wybranych list", "success") - return redirect(url_for("admin_mass_edit_categories")) + return redirect(url_for("admin_mass_edit_categories", page=page, per_page=per_page)) + + query_string = urlencode({k: v for k, v in request.args.items() if k != "page"}) return render_template( "admin/mass_edit_categories.html", lists=lists, - categories=categories + categories=categories, + page=page, + per_page=per_page, + total_pages=pagination.pages, + total_items=pagination.total, + query_string=query_string, ) @@ -3038,7 +3049,17 @@ def admin_list_items_json(list_id): for item in l.items ] - return jsonify({"title": l.title, "items": items}) + purchased_count = sum(1 for item in l.items if item.purchased) + total_expense = sum(exp.amount for exp in l.expenses) + + return jsonify({ + "title": l.title, + "items": items, + "total_count": len(l.items), + "purchased_count": purchased_count, + "total_expense": round(total_expense, 2) + }) + @app.route("/healthcheck") diff --git a/static/js/preview_list_modal.js b/static/js/preview_list_modal.js index 31dabfd..e08036c 100644 --- a/static/js/preview_list_modal.js +++ b/static/js/preview_list_modal.js @@ -2,10 +2,10 @@ document.addEventListener("DOMContentLoaded", function () { const modalElement = document.getElementById("productPreviewModal"); const modal = new bootstrap.Modal(modalElement); - modalElement.addEventListener('hidden.bs.modal', function () { - document.querySelectorAll('.modal-backdrop').forEach(el => el.remove()); - document.body.classList.remove('modal-open'); - document.body.style.overflow = ''; + modalElement.addEventListener("hidden.bs.modal", function () { + document.querySelectorAll(".modal-backdrop").forEach((el) => el.remove()); + document.body.classList.remove("modal-open"); + document.body.style.overflow = ""; }); document.querySelectorAll(".preview-btn").forEach((btn) => { @@ -15,7 +15,10 @@ document.addEventListener("DOMContentLoaded", function () { const productList = document.getElementById("product-list"); modalTitle.textContent = "Ładowanie..."; - productList.innerHTML = '
  • ⏳ Ładowanie produktów...
  • '; + productList.innerHTML = ` +
  • + ⏳ Ładowanie produktów... +
  • `; modal.show(); @@ -26,6 +29,24 @@ document.addEventListener("DOMContentLoaded", function () { modalTitle.textContent = `🛒 ${data.title}`; productList.innerHTML = ""; + // 🔢 PODSUMOWANIE + const summary = document.createElement("div"); + summary.className = "mb-3"; + + const percent = + data.total_count > 0 + ? Math.round((data.purchased_count / data.total_count) * 100) + : 0; + + summary.innerHTML = ` +

    📦 ${data.total_count} produktów

    +

    ✅ Kupione: ${data.purchased_count} (${percent}%)

    +

    💸 Wydatek: ${data.total_expense.toFixed(2)} zł

    +
    + `; + productList.appendChild(summary); + + // 🛒 LISTY PRODUKTÓW const purchasedList = document.createElement("ul"); purchasedList.className = "list-group list-group-flush mb-3"; @@ -35,14 +56,20 @@ document.addEventListener("DOMContentLoaded", function () { let hasPurchased = false; let hasUnpurchased = false; - data.items.forEach(item => { + data.items.forEach((item) => { const li = document.createElement("li"); - li.className = "list-group-item bg-dark text-white d-flex justify-content-between"; + li.className = + "list-group-item bg-dark text-white d-flex justify-content-between"; li.innerHTML = ` - ${item.name} - - x${item.quantity} - `; + ${item.name} + + x${item.quantity} + `; if (item.purchased) { purchasedList.appendChild(li); @@ -68,13 +95,18 @@ document.addEventListener("DOMContentLoaded", function () { } if (!hasPurchased && !hasUnpurchased) { - productList.innerHTML = '
  • Brak produktów
  • '; + productList.innerHTML = ` +
  • + Brak produktów +
  • `; } - } catch (err) { modalTitle.textContent = "Błąd"; - productList.innerHTML = '
  • ❌ Błąd podczas ładowania
  • '; + productList.innerHTML = ` +
  • + ❌ Błąd podczas ładowania +
  • `; } }); }); -}); \ No newline at end of file +}); diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index d6af44d..dc2a95b 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -4,7 +4,7 @@

    🛠️ Edytuj listę #{{ list.id }}

    - ← Powrót + ← Powrót do panelu
    diff --git a/templates/admin/list_products.html b/templates/admin/list_products.html index e05b195..a48c88c 100644 --- a/templates/admin/list_products.html +++ b/templates/admin/list_products.html @@ -4,7 +4,7 @@

    🛍️ Produkty i sugestie

    - ← Powrót do panelu + ← Powrót do panelu
    @@ -16,7 +16,7 @@ {{ items|length }} produktów
    - +
    @@ -85,7 +85,7 @@
    {% set item_names = items | map(attribute='name') | map('lower') | list %} -
    ID
    +
    @@ -117,6 +117,34 @@ +
    +
    + + + + + + +
    + - {% if orphan_files and request.path.endswith('/all') %}
    diff --git a/templates/admin/user_management.html b/templates/admin/user_management.html index a6aac15..9467a74 100644 --- a/templates/admin/user_management.html +++ b/templates/admin/user_management.html @@ -4,7 +4,7 @@

    👥 Zarządzanie użytkownikami

    - ← Powrót do panelu + ← Powrót do panelu
    ID