From 0a44753eb2b7b2a5f37449344742bceee4484ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 31 Jul 2025 10:37:44 +0200 Subject: [PATCH] poprawki w panelu, kategorie na wykresach i inne --- .env.example | 12 +- app.py | 356 +++++++++++++++++++++------- config.py | 10 + static/js/user_expenses.js | 75 ++++-- templates/admin/admin_panel.html | 383 ++++++++++++++++++------------- templates/user_expenses.html | 77 ++++--- 6 files changed, 621 insertions(+), 292 deletions(-) diff --git a/.env.example b/.env.example index 6f2192c..fe92534 100644 --- a/.env.example +++ b/.env.example @@ -158,4 +158,14 @@ LIB_CSS_CACHE_CONTROL="public, max-age=604800" # UPLOADS_CACHE_CONTROL: # Nagłówki Cache-Control dla wgrywanych plików (/uploads/) # Domyślnie: "public, max-age=2592000, immutable" -UPLOADS_CACHE_CONTROL="public, max-age=2592000, immutable" \ No newline at end of file +UPLOADS_CACHE_CONTROL="public, max-age=2592000, immutable" + +# DEFAULT_CATEGORIES: +# Lista domyślnych kategorii tworzonych automatycznie przy starcie aplikacji, +# jeśli nie istnieją w bazie danych. +# Podaj w formacie CSV (oddzielone przecinkami) – kolejność zostanie zachowana. +# Możesz dodać własne kategorie +# UWAGA: Wielkość liter w nazwach jest zachowywana, ale porównywanie odbywa się +# bez rozróżniania wielkości liter (case-insensitive). +# Domyślnie: poniższa lista +DEFAULT_CATEGORIES="Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie,Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny,Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie,RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo" diff --git a/app.py b/app.py index f5c616f..21bb029 100644 --- a/app.py +++ b/app.py @@ -89,9 +89,9 @@ referrer_policy = app.config.get("REFERRER_POLICY") if referrer_policy: talisman_kwargs["referrer_policy"] = referrer_policy -talisman = Talisman(app, - session_cookie_secure=app.config["SESSION_COOKIE_SECURE"], - **talisman_kwargs) +talisman = Talisman( + app, session_cookie_secure=app.config["SESSION_COOKIE_SECURE"], **talisman_kwargs +) register_heif_opener() # pillow_heif dla HEIC SQLALCHEMY_ECHO = True @@ -109,7 +109,7 @@ SESSION_COOKIE_SECURE = app.config.get("SESSION_COOKIE_SECURE") app.config["COMPRESS_ALGORITHM"] = ["zstd", "br", "gzip", "deflate"] app.config["PERMANENT_SESSION_LIFETIME"] = timedelta(minutes=SESSION_TIMEOUT_MINUTES) -#app.config["SESSION_COOKIE_SECURE"] = True if app.config.get("SESSION_COOKIE_SECURE") is True else False +# app.config["SESSION_COOKIE_SECURE"] = True if app.config.get("SESSION_COOKIE_SECURE") is True else False app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) DEBUG_MODE = app.config.get("DEBUG_MODE", False) @@ -166,14 +166,23 @@ class User(UserMixin, db.Model): # Tabela pośrednia shopping_list_category = db.Table( "shopping_list_category", - db.Column("shopping_list_id", db.Integer, db.ForeignKey("shopping_list.id"), primary_key=True), - db.Column("category_id", db.Integer, db.ForeignKey("category.id"), primary_key=True) + db.Column( + "shopping_list_id", + db.Integer, + db.ForeignKey("shopping_list.id"), + primary_key=True, + ), + db.Column( + "category_id", db.Integer, db.ForeignKey("category.id"), primary_key=True + ), ) + class Category(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(100), unique=True, nullable=False) + class ShoppingList(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(150), nullable=False) @@ -198,9 +207,10 @@ class ShoppingList(db.Model): categories = db.relationship( "Category", secondary=shopping_list_category, - backref=db.backref("shopping_lists", lazy="dynamic") + backref=db.backref("shopping_lists", lazy="dynamic"), ) + class Item(db.Model): id = db.Column(db.Integer, primary_key=True) list_id = db.Column(db.Integer, db.ForeignKey("shopping_list.id")) @@ -255,9 +265,14 @@ def handle_db_error(e): app.logger.error(f"[Błąd DB] {e}") if request.accept_mimetypes.best == "application/json": - return jsonify({ - "error": "Baza danych jest obecnie niedostępna. Spróbuj ponownie później." - }), 503 + return ( + jsonify( + { + "error": "Baza danych jest obecnie niedostępna. Spróbuj ponownie później." + } + ), + 503, + ) return ( render_template( @@ -288,6 +303,7 @@ def check_password(stored_hash, password_input): return False return False + def set_authorized_cookie(response): secure_flag = app.config["SESSION_COOKIE_SECURE"] # wartość z config.py @@ -301,7 +317,7 @@ def set_authorized_cookie(response): secure=secure_flag, httponly=True, samesite="Lax", - path="/" + path="/", ) return response @@ -331,32 +347,25 @@ with app.app_context(): print(f"[INFO] Zmieniono hasło admina '{admin_username}' z konfiguracji.") db.session.commit() else: - db.session.add(User( - username=admin_username, - password_hash=password_hash, - is_admin=True - )) + db.session.add( + User(username=admin_username, password_hash=password_hash, is_admin=True) + ) db.session.commit() # --- Predefiniowane kategorie --- - default_categories = [ - "Spożywcze", "Budowlane", "Zabawki", "Chemia", "Inne", - "Elektronika", "Odzież i obuwie", "Artykuły biurowe", - "Kosmetyki i higiena", "Motoryzacja", "Ogród i rośliny", - "Zwierzęta", "Sprzęt sportowy", "Książki i prasa", - "Narzędzia i majsterkowanie", "RTV / AGD", "Apteka i suplementy", - "Artykuły dekoracyjne", "Gry i hobby", "Usługi", "Pieczywo" - ] + default_categories = app.config["DEFAULT_CATEGORIES"] - # Pobierz istniejące nazwy z bazy, ignorując puste/niewłaściwe rekordy existing_names = { c.name for c in Category.query.filter(Category.name.isnot(None)).all() } - # Znajdź brakujące - missing = [cat for cat in default_categories if cat not in existing_names] + # ignorujemy wielkość liter przy porównaniu + existing_names_lower = {name.lower() for name in existing_names} + + missing = [ + cat for cat in default_categories if cat.lower() not in existing_names_lower + ] - # Dodaj tylko brakujące if missing: db.session.add_all(Category(name=cat) for cat in missing) db.session.commit() @@ -656,7 +665,7 @@ def get_total_expenses_grouped_by_list_created_at( if category_id: lists_query = lists_query.join( shopping_list_category, - shopping_list_category.c.shopping_list_id == ShoppingList.id + shopping_list_category.c.shopping_list_id == ShoppingList.id, ).filter(shopping_list_category.c.category_id == category_id) if start_date and end_date: @@ -737,6 +746,158 @@ def recalculate_filesizes(receipt_id: int = None): return updated, unchanged, not_found +def get_admin_expense_summary(): + now = datetime.now(timezone.utc) + current_year = now.year + current_month = now.month + + def calc_sum(base_query): + total = base_query.scalar() or 0 + year_total = ( + base_query.filter( + extract("year", Expense.added_at) == current_year + ).scalar() + or 0 + ) + month_total = ( + base_query.filter(extract("year", Expense.added_at) == current_year) + .filter(extract("month", Expense.added_at) == current_month) + .scalar() + or 0 + ) + return {"total": total, "year": year_total, "month": month_total} + + # baza wspólna + base = db.session.query(func.sum(Expense.amount)).join( + ShoppingList, ShoppingList.id == Expense.list_id + ) + + # wszystkie listy + all_lists = calc_sum(base) + + # aktywne listy + active_lists = calc_sum( + base.filter( + ShoppingList.is_archived == False, + or_(ShoppingList.expires_at == None, ShoppingList.expires_at > now), + ) + ) + + # archiwalne + archived_lists = calc_sum(base.filter(ShoppingList.is_archived == True)) + + # wygasłe + expired_lists = calc_sum( + base.filter( + ShoppingList.is_archived == False, + ShoppingList.expires_at != None, + ShoppingList.expires_at <= now, + ) + ) + + return { + "all": all_lists, + "active": active_lists, + "archived": archived_lists, + "expired": expired_lists, + } + + +def category_to_color(name): + """Generuje powtarzalny pastelowy kolor HEX na podstawie nazwy kategorii.""" + hash_val = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16) + r = (hash_val & 0xFF0000) >> 16 + g = (hash_val & 0x00FF00) >> 8 + b = hash_val & 0x0000FF + # Rozjaśnienie (pastel) + r = (r + 255) // 2 + g = (g + 255) // 2 + b = (b + 255) // 2 + return f"#{r:02x}{g:02x}{b:02x}" + + +def get_total_expenses_grouped_by_category( + show_all, range_type, start_date, end_date, user_id +): + lists_query = ShoppingList.query + + if show_all: + lists_query = lists_query.filter( + or_(ShoppingList.owner_id == user_id, ShoppingList.is_public == True) + ) + else: + lists_query = lists_query.filter(ShoppingList.owner_id == user_id) + + if start_date and end_date: + try: + dt_start = datetime.strptime(start_date, "%Y-%m-%d") + dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1) + except Exception: + return {"error": "Błędne daty"} + lists_query = lists_query.filter( + ShoppingList.created_at >= dt_start, ShoppingList.created_at < dt_end + ) + + lists = lists_query.options(joinedload(ShoppingList.categories)).all() + if not lists: + return {"labels": [], "datasets": []} + + data_map = defaultdict(lambda: defaultdict(float)) + all_labels = set() + + for l in lists: + total_expense = ( + db.session.query(func.sum(Expense.amount)) + .filter(Expense.list_id == l.id) + .scalar() + ) or 0 + + if total_expense <= 0: + continue + + if range_type == "monthly": + key = l.created_at.strftime("%Y-%m") + elif range_type == "quarterly": + key = f"{l.created_at.year}-Q{((l.created_at.month - 1) // 3 + 1)}" + elif range_type == "halfyearly": + key = f"{l.created_at.year}-H{1 if l.created_at.month <= 6 else 2}" + elif range_type == "yearly": + key = str(l.created_at.year) + else: + key = l.created_at.strftime("%Y-%m-%d") + + all_labels.add(key) + + if not l.categories: + data_map[key]["Inne"] += total_expense + else: + for c in l.categories: + data_map[key][c.name] += total_expense + + labels = sorted(all_labels) + + categories_with_expenses = sorted( + { + cat + for cat_data in data_map.values() + for cat, value in cat_data.items() + if value > 0 + } + ) + + datasets = [] + for cat in categories_with_expenses: + datasets.append( + { + "label": cat, + "data": [round(data_map[label].get(cat, 0), 2) for label in labels], + "backgroundColor": category_to_color(cat), + } + ) + + return {"labels": labels, "datasets": datasets} + + ############# OCR ########################### @@ -1143,8 +1304,7 @@ def main_page(): # ostatnia kwota (w tym przypadku max = suma z ostatniego zapisu) latest_expenses_map = dict( db.session.query( - Expense.list_id, - func.coalesce(func.sum(Expense.amount), 0) + Expense.list_id, func.coalesce(func.sum(Expense.amount), 0) ) .filter(Expense.list_id.in_(all_ids)) .group_by(Expense.list_id) @@ -1185,7 +1345,10 @@ def system_auth(): next_page = request.args.get("next") or url_for("main_page") if is_ip_blocked(ip): - flash("Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.", "danger") + flash( + "Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.", + "danger", + ) return render_template("system_auth.html"), 403 if request.method == "POST": @@ -1196,7 +1359,10 @@ def system_auth(): else: register_failed_attempt(ip) if is_ip_blocked(ip): - flash("Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.", "danger") + flash( + "Przekroczono limit prób logowania. Dostęp zablokowany na 1 godzinę.", + "danger", + ) return render_template("system_auth.html"), 403 remaining = attempts_remaining(ip) flash(f"Nieprawidłowe hasło. Pozostało {remaining} prób.", "warning") @@ -1306,11 +1472,10 @@ def edit_my_list(list_id): list=l, receipts=receipts, categories=categories, - selected_categories=selected_categories_ids + selected_categories=selected_categories_ids, ) - @app.route("/delete_user_list/", methods=["POST"]) @login_required def delete_user_list(list_id): @@ -1452,7 +1617,28 @@ def user_expenses(): category_id = request.args.get("category_id", type=int) show_all = request.args.get("show_all", "true").lower() == "true" - categories = Category.query.order_by(Category.name.asc()).all() + categories = ( + Category.query.join( + shopping_list_category, shopping_list_category.c.category_id == Category.id + ) + .join( + ShoppingList, ShoppingList.id == shopping_list_category.c.shopping_list_id + ) + .join(Expense, Expense.list_id == ShoppingList.id) + .filter( + or_( + ShoppingList.owner_id == current_user.id, + ( + ShoppingList.is_public == True + if show_all + else ShoppingList.owner_id == current_user.id + ), + ) + ) + .distinct() + .order_by(Category.name.asc()) + .all() + ) start = None end = None @@ -1468,15 +1654,14 @@ def user_expenses(): else: expenses_query = expenses_query.filter( or_( - ShoppingList.owner_id == current_user.id, - ShoppingList.is_public == True + ShoppingList.owner_id == current_user.id, ShoppingList.is_public == True ) ) if category_id: expenses_query = expenses_query.join( shopping_list_category, - shopping_list_category.c.shopping_list_id == ShoppingList.id + shopping_list_category.c.shopping_list_id == ShoppingList.id, ).filter(shopping_list_category.c.category_id == category_id) if start_date_str and end_date_str: @@ -1522,7 +1707,7 @@ def user_expenses(): "created_at": l.created_at, "total_expense": totals_map.get(l.id, 0), "owner_username": l.owner.username if l.owner else "?", - "categories": [c.id for c in l.categories] + "categories": [c.id for c in l.categories], } for l in {e.shopping_list for e in expenses if e.shopping_list} ] @@ -1545,17 +1730,27 @@ def user_expenses_data(): end_date = request.args.get("end_date") show_all = request.args.get("show_all", "true").lower() == "true" category_id = request.args.get("category_id", type=int) + by_category = request.args.get("by_category", "false").lower() == "true" - result = get_total_expenses_grouped_by_list_created_at( - user_only=True, - admin=False, - show_all=show_all, - range_type=range_type, - start_date=start_date, - end_date=end_date, - user_id=current_user.id, - category_id=category_id - ) + if by_category: + result = get_total_expenses_grouped_by_category( + show_all=show_all, + range_type=range_type, + start_date=start_date, + end_date=end_date, + user_id=current_user.id, + ) + else: + result = get_total_expenses_grouped_by_list_created_at( + user_only=True, + admin=False, + show_all=show_all, + range_type=range_type, + start_date=start_date, + end_date=end_date, + user_id=current_user.id, + category_id=category_id, + ) if "error" in result: return jsonify({"error": result["error"]}), 400 @@ -1640,12 +1835,14 @@ def all_products(): ) top_products = ( - top_products_query.order_by( - SuggestedProduct.name.asc(), # musi być pierwsze - SuggestedProduct.usage_count.desc(), + db.session.query( + func.lower(Item.name).label("name"), func.sum(Item.quantity).label("count") ) - .distinct(SuggestedProduct.name) - .limit(20) + .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() ) @@ -1887,6 +2084,7 @@ def admin_panel(): joinedload(ShoppingList.items), joinedload(ShoppingList.receipts), joinedload(ShoppingList.expenses), + joinedload(ShoppingList.categories), ).all() all_ids = [l.id for l in all_lists] @@ -1914,15 +2112,13 @@ def admin_panel(): latest_expenses_map = dict( db.session.query( - Expense.list_id, - func.coalesce(func.sum(Expense.amount), 0) + Expense.list_id, func.coalesce(func.sum(Expense.amount), 0) ) .filter(Expense.list_id.in_(all_ids)) .group_by(Expense.list_id) .all() ) - enriched_lists = [] for l in all_lists: total_count, purchased_count = stats_map.get(l.id, (0, 0)) @@ -1949,6 +2145,7 @@ def admin_panel(): "receipts_count": receipts_count, "total_expense": total_expense, "expired": is_expired, + "categories": l.categories, } ) @@ -1964,24 +2161,8 @@ def admin_panel(): purchased_items_count = Item.query.filter_by(purchased=True).count() - # Podsumowania wydatków globalnych - total_expense_sum = db.session.query(func.sum(Expense.amount)).scalar() or 0 - current_time = datetime.now(timezone.utc) - current_year = current_time.year - current_month = current_time.month - - year_expense_sum = ( - db.session.query(func.sum(Expense.amount)) - .filter(extract("year", Expense.added_at) == current_year) - .scalar() - ) or 0 - - month_expense_sum = ( - db.session.query(func.sum(Expense.amount)) - .filter(extract("year", Expense.added_at) == current_year) - .filter(extract("month", Expense.added_at) == current_month) - .scalar() - ) or 0 + # Nowe podsumowanie wydatków + expense_summary = get_admin_expense_summary() # Statystyki systemowe process = psutil.Process(os.getpid()) @@ -1996,9 +2177,7 @@ def admin_panel(): inspector = inspect(db_engine) table_count = len(inspector.get_table_names()) - record_total = get_total_records() - uptime_minutes = int( (datetime.now(timezone.utc) - app_start_time).total_seconds() // 60 ) @@ -2011,9 +2190,7 @@ def admin_panel(): purchased_items_count=purchased_items_count, enriched_lists=enriched_lists, top_products=top_products, - total_expense_sum=total_expense_sum, - year_expense_sum=year_expense_sum, - month_expense_sum=month_expense_sum, + expense_summary=expense_summary, now=now, python_version=sys.version, system_info=platform.platform(), @@ -2025,7 +2202,6 @@ def admin_panel(): ) - @app.route("/admin/delete_list/") @login_required @admin_required @@ -2291,7 +2467,7 @@ def edit_list(list_id): joinedload(ShoppingList.owner), joinedload(ShoppingList.items), joinedload(ShoppingList.categories), - ] + ], ) if l is None: @@ -2378,7 +2554,7 @@ def edit_list(list_id): db.session.commit() flash("Zapisano zmiany listy", "success") return redirect(url_for("edit_list", list_id=list_id)) - + elif action == "add_item": item_name = request.form.get("item_name", "").strip() quantity_str = request.form.get("quantity", "1") @@ -2487,7 +2663,7 @@ def edit_list(list_id): items=items, receipts=receipts, categories=categories, - selected_categories=selected_categories_ids + selected_categories=selected_categories_ids, ) @@ -2640,7 +2816,11 @@ def recalculate_filesizes_all(): @login_required @admin_required def admin_mass_edit_categories(): - lists = ShoppingList.query.options(joinedload(ShoppingList.categories)).order_by(ShoppingList.created_at.desc()).all() + lists = ( + ShoppingList.query.options(joinedload(ShoppingList.categories)) + .order_by(ShoppingList.created_at.desc()) + .all() + ) categories = Category.query.order_by(Category.name.asc()).all() if request.method == "POST": @@ -2654,7 +2834,9 @@ def admin_mass_edit_categories(): flash("Zaktualizowano kategorie dla wybranych list", "success") return redirect(url_for("admin_mass_edit_categories")) - return render_template("admin/mass_edit_categories.html", lists=lists, categories=categories) + return render_template( + "admin/mass_edit_categories.html", lists=lists, categories=categories + ) @app.route("/healthcheck") diff --git a/config.py b/config.py index 0b3e32a..eaeafe7 100644 --- a/config.py +++ b/config.py @@ -71,3 +71,13 @@ class Config: UPLOADS_CACHE_CONTROL = os.environ.get( "UPLOADS_CACHE_CONTROL", "public, max-age=2592000, immutable" ) + + DEFAULT_CATEGORIES = [ + c.strip() for c in os.environ.get( + "DEFAULT_CATEGORIES", + "Spożywcze,Budowlane,Zabawki,Chemia,Inne,Elektronika,Odzież i obuwie," + "Artykuły biurowe,Kosmetyki i higiena,Motoryzacja,Ogród i rośliny," + "Zwierzęta,Sprzęt sportowy,Książki i prasa,Narzędzia i majsterkowanie," + "RTV / AGD,Apteka i suplementy,Artykuły dekoracyjne,Gry i hobby,Usługi,Pieczywo" + ).split(",") if c.strip() + ] \ No newline at end of file diff --git a/static/js/user_expenses.js b/static/js/user_expenses.js index 769b432..9519554 100644 --- a/static/js/user_expenses.js +++ b/static/js/user_expenses.js @@ -1,6 +1,7 @@ document.addEventListener("DOMContentLoaded", function () { let expensesChart = null; let selectedCategoryId = ""; + let categorySplit = false; // <-- nowy tryb const rangeLabel = document.getElementById("chartRangeLabel"); function loadExpenses(range = "monthly", startDate = null, endDate = null) { @@ -15,6 +16,9 @@ document.addEventListener("DOMContentLoaded", function () { if (selectedCategoryId) { url += `&category_id=${selectedCategoryId}`; } + if (categorySplit) { + url += '&by_category=true'; + } fetch(url, { cache: "no-store" }) .then(response => response.json()) @@ -25,24 +29,44 @@ document.addEventListener("DOMContentLoaded", function () { expensesChart.destroy(); } - expensesChart = new Chart(ctx, { - type: 'bar', - data: { - labels: data.labels, - datasets: [{ - label: 'Suma wydatków [PLN]', - data: data.expenses, - backgroundColor: '#0d6efd' - }] - }, - options: { - scales: { - y: { - beginAtZero: true + if (categorySplit) { + // Tryb z podziałem na kategorie + expensesChart = new Chart(ctx, { + type: 'bar', + data: { + labels: data.labels, + datasets: data.datasets // <-- gotowe z backendu + }, + options: { + responsive: true, + plugins: { + tooltip: { mode: 'index', intersect: false }, + legend: { position: 'top' } + }, + scales: { + x: { stacked: true }, + y: { stacked: true, beginAtZero: true } } } - } - }); + }); + } else { + // Tryb zwykły + expensesChart = new Chart(ctx, { + type: 'bar', + data: { + labels: data.labels, + datasets: [{ + label: 'Suma wydatków [PLN]', + data: data.expenses, + backgroundColor: '#0d6efd' + }] + }, + options: { + responsive: true, + scales: { y: { beginAtZero: true } } + } + }); + } if (startDate && endDate) { rangeLabel.textContent = `Widok: własny zakres (${startDate} → ${endDate})`; @@ -54,13 +78,28 @@ document.addEventListener("DOMContentLoaded", function () { else if (range === "yearly") labelText = "Widok: roczne"; rangeLabel.textContent = labelText; } - }) .catch(error => { console.error("Błąd pobierania danych:", error); }); } + // Obsługa przycisku przełączania trybu + document.getElementById("toggleCategorySplit").addEventListener("click", function () { + categorySplit = !categorySplit; + if (categorySplit) { + this.textContent = "🔵 Pokaż całościowo"; + this.classList.remove("btn-outline-warning"); + this.classList.add("btn-outline-info"); + } else { + this.textContent = "🎨 Pokaż podział na kategorie"; + this.classList.remove("btn-outline-info"); + this.classList.add("btn-outline-warning"); + } + loadExpenses(); // przeładuj wykres + }); + + // Reszta Twojego kodu bez zmian... const startDateInput = document.getElementById("startDate"); const endDateInput = document.getElementById("endDate"); const today = new Date(); @@ -97,7 +136,7 @@ document.addEventListener("DOMContentLoaded", function () { document.querySelectorAll('.category-filter').forEach(b => b.classList.remove('active')); this.classList.add('active'); selectedCategoryId = this.dataset.categoryId || ""; - loadExpenses(); // odśwież wykres z nowym filtrem + loadExpenses(); }); }); }); diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 0d777be..fd839e2 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -7,70 +7,117 @@ ← Powrót do strony głównej - +
+
-

👤 Liczba użytkowników: {{ user_count }}

-

📝 Liczba list zakupowych: {{ list_count }}

-

🛒 Liczba produktów: {{ item_count }}

-

✅ Zakupionych produktów: {{ purchased_items_count }}

+
📊 Statystyki ogólne
+ + + + + + + + + + + + + + + + + + + +
👤 Użytkownicy{{ user_count }}
📝 Listy zakupowe{{ list_count }}
🛒 Produkty{{ item_count }}
✅ Zakupione{{ purchased_items_count }}
- {% if top_products %} +
-
🔥 Najczęściej kupowane produkty:
-
    - {% for name, count in top_products %} -
  • {{ name }} — {{ count }}×
  • - {% endfor %} -
+
🔥 Najczęściej kupowane produkty
+ {% if top_products %} + {% set max_count = top_products[0][1] %} + {% for name, count in top_products %} +
+
+ {{ name }} + {{ count }}× +
+
+
+
+
+
+ {% endfor %} + {% else %} +

Brak danych

+ {% endif %}
- {% endif %} +
💸 Podsumowanie wydatków:
-
    -
  • Obecny miesiąc: {{ '%.2f'|format(month_expense_sum) }} PLN
  • -
  • Obecny rok: {{ '%.2f'|format(year_expense_sum) }} PLN
  • -
  • Całkowite: {{ '%.2f'|format(total_expense_sum) }} PLN
  • -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Typ listyMiesiącRokCałkowite
Wszystkie{{ '%.2f'|format(expense_summary.all.month) }} PLN{{ '%.2f'|format(expense_summary.all.year) }} PLN{{ '%.2f'|format(expense_summary.all.total) }} PLN
Aktywne{{ '%.2f'|format(expense_summary.active.month) }} PLN{{ '%.2f'|format(expense_summary.active.year) }} PLN{{ '%.2f'|format(expense_summary.active.total) }} PLN
Archiwalne{{ '%.2f'|format(expense_summary.archived.month) }} PLN{{ '%.2f'|format(expense_summary.archived.year) }} PLN{{ '%.2f'|format(expense_summary.archived.total) }} PLN
Wygasłe{{ '%.2f'|format(expense_summary.expired.month) }} PLN{{ '%.2f'|format(expense_summary.expired.year) }} PLN{{ '%.2f'|format(expense_summary.expired.total) }} PLN
+
+
-
-
-

📄 Wszystkie listy zakupowe

-
-
- - - - - - - - - - - - - - - - - - - {% for e in enriched_lists %} - {% set l = e.list %} - - - - - - - - - - - - - - - {% endfor %} - +
+
+

📄 Wszystkie listy zakupowe

+ +
+
IDTytułStatusUtworzonoWłaścicielProduktyWypełnienieKomentarzeParagonyWydatkiAkcje
{{ l.id }} - {{ l.title }} - - {% if l.is_archived %} - Archiwalna - {% elif e.expired %} - Wygasła - {% else %} - Aktywna - {% endif %} - {{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }} - {% if l.owner_id %} - {{ l.owner_id }} / {{ l.owner.username if l.owner else 'Brak użytkownika' }} - {% else %} - - - {% endif %} - {{ e.total_count }}{{ e.purchased_count }}/{{ e.total_count }} ({{ e.percent }}%){{ e.comments_count }}{{ e.receipts_count }} - {% if e.total_expense > 0 %} - {{ '%.2f'|format(e.total_expense) }} PLN - {% else %} - - - {% endif %} - - ✏️ - Edytuj - 🗑️ - Usuń -
+ + + + + + + + + + + + + + + + + + {% for e in enriched_lists %} + {% set l = e.list %} + + + + -
IDTytułStatusUtworzonoWłaścicielProduktyWypełnienieKomentarzeParagonyWydatkiAkcje
{{ l.id }} + {{ l.title }} + {% if l.categories %} + + 🏷 + + {% endif %} +
-
- -
-
+ + {% if l.is_archived %} + Archiwalna + {% elif e.expired %} + Wygasła + {% else %} + Aktywna + {% endif %} + + {{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }} + + {% if l.owner %} + 👤 {{ l.owner.username }} ({{ l.owner.id }}) + {% else %} + - + {% endif %} + + {{ e.total_count }} + +
+
+ {{ e.purchased_count }}/{{ e.total_count }} +
+
+ + {{ e.comments_count }} + {{ e.receipts_count }} + + {% if e.total_expense > 0 %} + {{ '%.2f'|format(e.total_expense) }} PLN + {% else %} + - + {% endif %} + + + ✏️ Edytuj + 🗑️ Usuń + + + {% endfor %} + + +
+ + + -