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 = '
📦 ${data.total_count} produktów
+✅ Kupione: ${data.purchased_count} (${percent}%)
+💸 Wydatek: ${data.total_expense.toFixed(2)} zł
+ID | @@ -85,7 +85,7 @@
---|
ID | @@ -117,6 +117,34 @@ +
---|