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 01/90] 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 %} From 5c941ea955e563cdb0e4191499605c7935d006b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 22:55:40 +0200 Subject: [PATCH 17/90] sortowalna tabela userow --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8b5e4f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.py text working-tree-encoding=UTF-8 +*.env.example text working-tree-encoding=UTF-8 +.env text working-tree-encoding=UTF-8 From 2d22fd2583884c443e1e85ce2810683d04524f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:04:35 +0200 Subject: [PATCH 18/90] update .gitignore --- .gitignore | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4ba6499..00d8011 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,9 @@ venv env *.db __pycache__ -instance/ -database/ uploads/ .DS_Store -db/* +db/mysql/* +db/pgsql/* +db/shopping.db *.swp \ No newline at end of file From 25d1967fd8e61ff62cddfcb1f028daf4af1a9800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:10:56 +0200 Subject: [PATCH 19/90] fix dla mysql --- app.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 11df39b..e05bf99 100644 --- a/app.py +++ b/app.py @@ -951,9 +951,11 @@ def save_pdf_as_webp(file, path): def get_active_months_query(visible_lists_query=None): - if db.engine.name == "sqlite": + if db.engine.name in ("sqlite",): month_col = func.strftime("%Y-%m", ShoppingList.created_at) - else: + elif db.engine.name in ("mysql", "mariadb"): + month_col = func.date_format(ShoppingList.created_at, "%Y-%m") + else: # PostgreSQL i inne wspierające to_char month_col = func.to_char(ShoppingList.created_at, "YYYY-MM") query = db.session.query(month_col.label("month")) From 5c90e020b6d87520b8fb7cae474644300a5c56b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:14:15 +0200 Subject: [PATCH 20/90] poprawka wizualna --- templates/admin/admin_panel.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 9b65e2c..28570af 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -59,7 +59,7 @@
    {{ name }} - {{ count }}× + {{ count }}×
    {% endfor %} {% else %} - Brak danych

    + Brak danych

    {% endif %}
    From 27e14fdd1d3cb8e373840830aaa05839c992e51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:15:19 +0200 Subject: [PATCH 21/90] poprawka wizualna --- templates/admin/admin_panel.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 28570af..4db337e 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -118,7 +118,7 @@ - + 📊 Pokaż wykres wydatków From 1e73d85600c09ea2deeb56d27bb400084890e854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:16:10 +0200 Subject: [PATCH 22/90] poprawka wizualna --- templates/admin/admin_panel.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 4db337e..bc74219 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -279,7 +279,7 @@
    - +
    From 16065df4c46f2db1c24877a5819c0743625ee155 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:17:10 +0200 Subject: [PATCH 23/90] poprawka wizualna --- templates/admin/admin_panel.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index bc74219..3f1d431 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -69,8 +69,10 @@ {% endfor %} {% else %} - Brak danych

    - {% endif %} +
    + Brak danych

    +
    + {% endif %} From 7a2685771dc40a6f4e6ba48c8708c66322c5c245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:22:00 +0200 Subject: [PATCH 24/90] poprawka wizualna --- templates/admin/admin_panel.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 3f1d431..c9fd6f9 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -70,7 +70,7 @@ {% endfor %} {% else %}
    - Brak danych

    +

    Brak danych

    {% endif %} From 0878b34047e8f6f0684386bdbb8d0b728ead2c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:29:02 +0200 Subject: [PATCH 25/90] poprawka wizualna --- templates/admin/edit_list.html | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index 5701187..980ac9b 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -117,7 +117,7 @@ value="{{ request.url_root }}share/{{ list.share_token }}"> - + @@ -175,7 +175,7 @@
    - +
    @@ -193,14 +193,15 @@
    {% if not item.not_purchased %} - - {% endif %} {% if item.not_purchased %} - {% endif %}
    @@ -210,7 +211,7 @@
    - +
    From 8b9483952e6c5bfa0b98d9c966fa046a85d07aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 16 Aug 2025 23:32:42 +0200 Subject: [PATCH 26/90] poprawka wizualna --- templates/admin/admin_panel.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index c9fd6f9..1110269 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -35,7 +35,7 @@ {{ list_count }} - 🛒 Produkty + 🛒 Produkty na listach {{ item_count }} @@ -59,7 +59,7 @@
    {{ name }} - {{ count }}× + {{ count }}x
    Date: Sun, 17 Aug 2025 17:07:43 +0200 Subject: [PATCH 27/90] poprawki i optymalizacje kodu --- app.py | 255 ++++++++++---- templates/admin/admin_panel.html | 410 ++++++++++++---------- templates/admin/edit_list.html | 2 +- templates/admin/list_products.html | 5 +- templates/admin/mass_edit_categories.html | 8 + templates/admin/receipts.html | 2 +- templates/admin/user_management.html | 5 +- templates/edit_my_list.html | 12 +- templates/expenses.html | 9 + 9 files changed, 436 insertions(+), 272 deletions(-) diff --git a/app.py b/app.py index e05bf99..45443ce 100644 --- a/app.py +++ b/app.py @@ -29,6 +29,7 @@ from flask import ( abort, session, jsonify, + g, ) from flask_sqlalchemy import SQLAlchemy from flask_login import ( @@ -44,7 +45,7 @@ from flask_socketio import SocketIO, emit, join_room from config import Config from PIL import Image, ExifTags, ImageFilter, ImageOps from werkzeug.middleware.proxy_fix import ProxyFix -from sqlalchemy import func, extract, inspect, or_, case, text +from sqlalchemy import func, extract, inspect, or_, case, text, and_ from sqlalchemy.orm import joinedload, load_only, aliased from collections import defaultdict, deque from functools import wraps @@ -219,10 +220,13 @@ class ShoppingList(db.Model): # Relacje items = db.relationship("Item", back_populates="shopping_list", lazy="select") - receipts = db.relationship("Receipt", back_populates="shopping_list", lazy="select") + receipts = db.relationship( + "Receipt", + back_populates="shopping_list", + cascade="all, delete-orphan", + lazy="select", + ) expenses = db.relationship("Expense", back_populates="shopping_list", lazy="select") - - # Nowa relacja wiele-do-wielu categories = db.relationship( "Category", secondary=shopping_list_category, @@ -270,7 +274,11 @@ class Expense(db.Model): class Receipt(db.Model): id = db.Column(db.Integer, primary_key=True) - list_id = db.Column(db.Integer, db.ForeignKey("shopping_list.id"), nullable=False) + list_id = db.Column( + db.Integer, + db.ForeignKey("shopping_list.id", ondelete="CASCADE"), + nullable=False, + ) filename = db.Column(db.String(255), nullable=False) uploaded_at = db.Column(db.DateTime, default=datetime.utcnow) filesize = db.Column(db.Integer, nullable=True) @@ -763,55 +771,70 @@ def get_admin_expense_summary(): current_year = now.year current_month = now.month - def calc_sum(base_query): - total = base_query.scalar() or 0 + def calc_summary(expense_query, list_query): + total = expense_query.scalar() or 0 year_total = ( - base_query.filter( + expense_query.filter( extract("year", ShoppingList.created_at) == current_year ).scalar() or 0 ) month_total = ( - base_query.filter(extract("year", ShoppingList.created_at) == current_year) - .filter(extract("month", ShoppingList.created_at) == current_month) - .scalar() + expense_query.filter( + extract("year", ShoppingList.created_at) == current_year, + extract("month", ShoppingList.created_at) == current_month, + ).scalar() or 0 ) - return {"total": total, "year": year_total, "month": month_total} + list_count = list_query.count() + avg = round(total / list_count, 2) if list_count else 0 + return { + "total": total, + "year": year_total, + "month": month_total, + "count": list_count, + "avg": avg, + } - base = db.session.query(func.sum(Expense.amount)).join( + expense_base = db.session.query(func.sum(Expense.amount)).join( ShoppingList, ShoppingList.id == Expense.list_id ) + list_base = ShoppingList.query - all_lists = calc_sum(base) + all = calc_summary(expense_base, list_base) - active_lists = calc_sum( - base.filter( - ShoppingList.is_archived == False, - ~( - (ShoppingList.is_temporary == True) - & (ShoppingList.expires_at != None) - & (ShoppingList.expires_at <= now) - ), - ) + active_condition = and_( + ShoppingList.is_archived == False, + ~( + (ShoppingList.is_temporary == True) + & (ShoppingList.expires_at != None) + & (ShoppingList.expires_at <= now) + ), + ) + active = calc_summary( + expense_base.filter(active_condition), list_base.filter(active_condition) ) - archived_lists = calc_sum(base.filter(ShoppingList.is_archived == True)) + archived_condition = ShoppingList.is_archived == True + archived = calc_summary( + expense_base.filter(archived_condition), list_base.filter(archived_condition) + ) - expired_lists = calc_sum( - base.filter( - ShoppingList.is_archived == False, - (ShoppingList.is_temporary == True), - (ShoppingList.expires_at != None), - (ShoppingList.expires_at <= now), - ) + expired_condition = and_( + ShoppingList.is_archived == False, + ShoppingList.is_temporary == True, + ShoppingList.expires_at != None, + ShoppingList.expires_at <= now, + ) + expired = calc_summary( + expense_base.filter(expired_condition), list_base.filter(expired_condition) ) return { - "all": all_lists, - "active": active_lists, - "archived": archived_lists, - "expired": expired_lists, + "all": all, + "active": active, + "archived": archived, + "expired": expired, } @@ -1205,7 +1228,7 @@ def require_system_password(): @app.before_request def start_timer(): - request._start_time = time.time() + g.start_time = time.time() @app.after_request @@ -1218,9 +1241,10 @@ def log_request(response): path = request.path status = response.status_code length = response.content_length or "-" - start = getattr(request, "_start_time", None) + start = getattr(g, "start_time", None) duration = round((time.time() - start) * 1000, 2) if start else "-" agent = request.headers.get("User-Agent", "-") + if status == 304: app.logger.info( f'REVALIDATED: {ip} - "{method} {path}" {status} {length} {duration}ms "{agent}"' @@ -1229,6 +1253,7 @@ def log_request(response): app.logger.info( f'{ip} - "{method} {path}" {status} {length} {duration}ms "{agent}"' ) + app.logger.debug(f"Request headers: {dict(request.headers)}") app.logger.debug(f"Response headers: {dict(response.headers)}") return response @@ -2014,14 +2039,31 @@ def all_products(): base_query = base_query.order_by("normalized_name") results = base_query.offset(offset).limit(limit).all() - total_count = ( db.session.query(func.count()).select_from(base_query.subquery()).scalar() ) products = [{"name": row.original_name, "count": row.count} for row in results] + used_names = set(row.original_name.strip().lower() for row in results) + extra_suggestions = ( + db.session.query(SuggestedProduct.name) + .filter(~func.lower(func.trim(SuggestedProduct.name)).in_(used_names)) + .all() + ) - return jsonify({"products": products, "total_count": total_count}) + suggested_fallbacks = [ + {"name": row.name.strip(), "count": 0} for row in extra_suggestions + ] + + if sort == "alphabetical": + products += suggested_fallbacks + products.sort(key=lambda x: x["name"].lower()) + else: + products += suggested_fallbacks + + return jsonify( + {"products": products, "total_count": total_count + len(suggested_fallbacks)} + ) @app.route("/upload_receipt/", methods=["POST"]) @@ -2251,7 +2293,6 @@ def admin_panel(): now = datetime.now(timezone.utc) start = end = None - # Liczniki globalne user_count = User.query.count() list_count = ShoppingList.query.count() item_count = Item.query.count() @@ -2270,17 +2311,12 @@ def admin_panel(): ) all_lists = base_query.all() - - # tylko listy z danych miesięcy - month_options = get_active_months_query() - all_ids = [l.id for l in all_lists] stats_map = {} latest_expenses_map = {} if all_ids: - # Statystyki produktów stats = ( db.session.query( Item.list_id, @@ -2336,17 +2372,57 @@ def admin_panel(): } ) + purchased_items_count = Item.query.filter_by(purchased=True).count() + not_purchased_count = Item.query.filter_by(not_purchased=True).count() + items_with_notes = Item.query.filter(Item.note.isnot(None), Item.note != "").count() + + total_expense = db.session.query(func.sum(Expense.amount)).scalar() or 0 + avg_list_expense = round(total_expense / list_count, 2) if list_count else 0 + + time_to_purchase = ( + db.session.query( + func.avg( + func.strftime("%s", Item.purchased_at) + - func.strftime("%s", Item.added_at) + ) + ) + .filter( + Item.purchased == True, + Item.purchased_at.isnot(None), + Item.added_at.isnot(None), + ) + .scalar() + ) + avg_hours_to_purchase = round(time_to_purchase / 3600, 2) if time_to_purchase else 0 + + first_list = db.session.query(func.min(ShoppingList.created_at)).scalar() + last_list = db.session.query(func.max(ShoppingList.created_at)).scalar() + now_dt = datetime.now(timezone.utc) + + if first_list and first_list.tzinfo is None: + first_list = first_list.replace(tzinfo=timezone.utc) + + if last_list and last_list.tzinfo is None: + last_list = last_list.replace(tzinfo=timezone.utc) + + if first_list and last_list: + days_span = max((now_dt - first_list).days, 1) + avg_per_day = list_count / days_span + avg_per_week = round(avg_per_day * 7, 2) + avg_per_month = round(avg_per_day * 30.44, 2) + avg_per_year = round(avg_per_day * 365, 2) + else: + avg_per_week = avg_per_month = avg_per_year = 0 + top_products = ( db.session.query(Item.name, func.count(Item.id).label("count")) .filter(Item.purchased.is_(True)) .group_by(Item.name) .order_by(func.count(Item.id).desc()) - .limit(5) + .limit(7) .all() ) - purchased_items_count = Item.query.filter_by(purchased=True).count() - expense_summary = get_admin_expense_summary() process = psutil.Process(os.getpid()) app_mem = process.memory_info().rss // (1024 * 1024) @@ -2365,12 +2441,21 @@ def admin_panel(): (datetime.now(timezone.utc) - app_start_time).total_seconds() // 60 ) + month_options = get_active_months_query() + return render_template( "admin/admin_panel.html", user_count=user_count, list_count=list_count, item_count=item_count, purchased_items_count=purchased_items_count, + not_purchased_count=not_purchased_count, + items_with_notes=items_with_notes, + avg_hours_to_purchase=avg_hours_to_purchase, + avg_list_expense=avg_list_expense, + avg_per_week=avg_per_week, + avg_per_month=avg_per_month, + avg_per_year=avg_per_year, enriched_lists=enriched_lists, top_products=top_products, expense_summary=expense_summary, @@ -2389,21 +2474,6 @@ def admin_panel(): ) -@app.route("/admin/delete_list/") -@login_required -@admin_required -def delete_list(list_id): - - delete_receipts_for_list(list_id) - list_to_delete = ShoppingList.query.get_or_404(list_id) - Item.query.filter_by(list_id=list_to_delete.id).delete() - Expense.query.filter_by(list_id=list_to_delete.id).delete() - db.session.delete(list_to_delete) - db.session.commit() - flash(f"Usunięto listę: {list_to_delete.title}", "success") - return redirect(url_for("admin_panel")) - - @app.route("/admin/add_user", methods=["POST"]) @login_required @admin_required @@ -2481,20 +2551,41 @@ def delete_user(user_id): user = User.query.get_or_404(user_id) if user.is_admin: - admin_count = User.query.filter_by(is_admin=True).count() - if admin_count <= 1: - flash("Nie można usunąć ostatniego administratora.", "danger") - return redirect(url_for("list_users")) + flash("Nie można usunąć konta administratora.", "warning") + return redirect(url_for("list_users")) + + admin_user = User.query.filter_by(is_admin=True).first() + if not admin_user: + flash("Brak konta administratora do przeniesienia zawartości.", "danger") + return redirect(url_for("list_users")) + + lists_owned = ShoppingList.query.filter_by(owner_id=user.id).count() + + if lists_owned > 0: + ShoppingList.query.filter_by(owner_id=user.id).update( + {"owner_id": admin_user.id} + ) + Receipt.query.filter_by(uploaded_by=user.id).update( + {"uploaded_by": admin_user.id} + ) + Item.query.filter_by(added_by=user.id).update({"added_by": admin_user.id}) + db.session.commit() + flash( + f"Użytkownik '{user.username}' został usunięty, a jego zawartość przeniesiona na administratora.", + "success", + ) + else: + flash( + f"Użytkownik '{user.username}' został usunięty. Nie posiadał żadnych list zakupowych.", + "info", + ) db.session.delete(user) db.session.commit() - flash("Użytkownik usunięty", "success") + return redirect(url_for("list_users")) -from sqlalchemy.orm import joinedload - - @app.route("/admin/receipts/") @login_required @admin_required @@ -2656,23 +2747,27 @@ def generate_receipt_hash(receipt_id): return redirect(request.referrer) -@app.route("/admin/delete_selected_lists", methods=["POST"]) +@app.route("/admin/delete_list", methods=["POST"]) @login_required @admin_required -def delete_selected_lists(): +def admin_delete_list(): ids = request.form.getlist("list_ids") + single_id = request.form.get("single_list_id") + if single_id: + ids.append(single_id) + for list_id in ids: - lst = db.session.get(ShoppingList, int(list_id)) - if lst: delete_receipts_for_list(lst.id) + Receipt.query.filter_by(list_id=lst.id).delete() Item.query.filter_by(list_id=lst.id).delete() Expense.query.filter_by(list_id=lst.id).delete() db.session.delete(lst) + db.session.commit() - flash("Usunięto wybrane listy", "success") - return redirect(url_for("admin_panel")) + flash(f"Usunięto {len(ids)} list(y)", "success") + return redirect(request.referrer or url_for("admin_panel")) @app.route("/admin/edit_list/", methods=["GET", "POST"]) @@ -2735,6 +2830,12 @@ def edit_list(list_id): user_obj = db.session.get(User, new_owner_id_int) if user_obj: shopping_list.owner_id = new_owner_id_int + Item.query.filter_by(list_id=list_id).update( + {"added_by": new_owner_id_int} + ) + Receipt.query.filter_by(list_id=list_id).update( + {"uploaded_by": new_owner_id_int} + ) else: flash("Wybrany użytkownik nie istnieje", "danger") return redirect(url_for("edit_list", list_id=list_id)) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 1110269..b13fcc3 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -42,12 +42,29 @@ ✅ Zakupione {{ purchased_items_count }} + + 🚫 Nieoznaczone jako kupione + {{ not_purchased_count }} + + + ✍️ Produkty z notatkami + {{ items_with_notes }} + + + 🕓 Śr. czas do zakupu (h) + {{ avg_hours_to_purchase }} + + + 💸 Średnia kwota na listę + {{ avg_list_expense }} zł +
    +
    @@ -79,17 +96,18 @@
    -
    +
    -
    💸 Podsumowanie wydatków:
    +
    💸 Podsumowanie wydatków
    - - +
    + - - - - + + + + + @@ -98,225 +116,247 @@ + + + +
    Typ listyMiesiącRokCałkowiteTyp listyMiesiącRokCałkowiteŚrednia
    {{ '%.2f'|format(expense_summary.all.month) }} PLN {{ '%.2f'|format(expense_summary.all.year) }} PLN {{ '%.2f'|format(expense_summary.all.total) }} PLN{{ '%.2f'|format(expense_summary.all.avg) }} PLN
    Aktywne {{ '%.2f'|format(expense_summary.active.month) }} PLN {{ '%.2f'|format(expense_summary.active.year) }} PLN {{ '%.2f'|format(expense_summary.active.total) }} PLN{{ '%.2f'|format(expense_summary.active.avg) }} PLN
    Archiwalne {{ '%.2f'|format(expense_summary.archived.month) }} PLN {{ '%.2f'|format(expense_summary.archived.year) }} PLN {{ '%.2f'|format(expense_summary.archived.total) }} PLN{{ '%.2f'|format(expense_summary.archived.avg) }} PLN
    Wygasłe {{ '%.2f'|format(expense_summary.expired.month) }} PLN {{ '%.2f'|format(expense_summary.expired.year) }} PLN {{ '%.2f'|format(expense_summary.expired.total) }} PLN{{ '%.2f'|format(expense_summary.expired.avg) }} PLN
    +
    +
    📈 Średnie tempo tworzenia list:
    +
      +
    • 📆 Tygodniowo: {{ avg_per_week }}
    • +
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • +
    • 📅 Rocznie: {{ avg_per_year }}
    • +
    +
    + 📊 Pokaż wykres wydatków -
    -
    -{# panel wyboru miesiąca zawsze widoczny #} -
    - {# LEWA STRONA — przyciski ← → TYLKO gdy nie show_all #} -
    - {% if not show_all %} - {% set current_date = now.replace(day=1) %} - {% set prev_month = (current_date - timedelta(days=1)).strftime('%Y-%m') %} - {% set next_month = (current_date + timedelta(days=31)).replace(day=1).strftime('%Y-%m') %} + {# panel wyboru miesiąca zawsze widoczny #} +
    - {% if prev_month in month_options %} - - ← {{ prev_month }} - - {% else %} - - {% endif %} + {# LEWA STRONA — przyciski ← → TYLKO gdy nie show_all #} +
    + {% if not show_all %} + {% set current_date = now.replace(day=1) %} + {% set prev_month = (current_date - timedelta(days=1)).strftime('%Y-%m') %} + {% set next_month = (current_date + timedelta(days=31)).replace(day=1).strftime('%Y-%m') %} - {% if next_month in month_options %} - - {{ next_month }} → - - {% else %} - - {% endif %} - {% else %} - {# Tryb wszystkie miesiące — możemy pokazać skrót do bieżącego miesiąca #} - - 📅 Przejdź do bieżącego miesiąca - - {% endif %} -
    - - {# PRAWA STRONA — picker miesięcy zawsze widoczny #} -
    -
    - 📅 - -
    -
    -
    - -
    -
    -

    - 📄 Listy zakupowe - {% if show_all %} - — wszystkie miesiące + {% if prev_month in month_options %} + + ← {{ prev_month }} + {% else %} - — {{ month_str|replace('-', ' / ') }} + {% endif %} -

    -
    -
    - - - - - - - - - - - - - - - - - - - {% for e in enriched_lists %} - {% set l = e.list %} - - - - - - - - - - - - - - - {% endfor %} - -
    IDTytułStatusUtworzonoWłaścicielProduktyProgressKoment.ParagonyWydatkiAkcje
    {{ 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 %} - -
    - ✏️ - - 🗑️ -
    -
    -
    -
    - + {% if next_month in month_options %} + + {{ next_month }} → + + {% else %} + + {% endif %} + {% else %} + {# Tryb wszystkie miesiące — możemy pokazać skrót do bieżącego miesiąca #} + + 📅 Przejdź do bieżącego miesiąca + + {% endif %} +
    + + {# PRAWA STRONA — picker miesięcy zawsze widoczny #} + +
    + 📅 +
    -
    -
    - Python: {{ python_version.split()[0] }} | {{ system_info }} | RAM app: {{ app_memory }} | - DB: {{ db_info.engine|upper }}{% if db_info.version %} v{{ db_info.version[0] }}{% endif %} | - Tabele: {{ table_count }} | Rekordy: {{ record_total }} | - Uptime: {{ uptime_minutes }} min -
    +
    +
    +

    + 📄 Listy zakupowe + {% if show_all %} + — wszystkie miesiące + {% else %} + — {{ month_str|replace('-', ' / ') }} + {% endif %} +

    +
    +
    + + + + + + + + + + + + + + + + + + + {% for e in enriched_lists %} + {% set l = e.list %} + + + + - - + + + + + + + + + + {% endfor %} + {% if enriched_lists|length == 0 %} + + + + {% endif %} + +
    IDTytułStatusUtworzonoWłaścicielProduktyProgressKoment.ParagonyWydatkiAkcje
    {{ 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 %} + +
    + ✏️ + + + + + +
    +
    + Brak list zakupowych do wyświetlenia +
    +
    +
    + +
    + +
    +
    + +
    + Python: {{ python_version.split()[0] }} | {{ system_info }} | RAM app: {{ app_memory }} | + DB: {{ db_info.engine|upper }}{% if db_info.version %} v{{ db_info.version[0] }}{% endif %} | + Tabele: {{ table_count }} | Rekordy: {{ record_total }} | + Uptime: {{ uptime_minutes }} min +
    + + + -
    -{% block scripts %} - - -{% endblock %} + {% block scripts %} + + + {% endblock %} -{% endblock %} \ No newline at end of file + {% endblock %} \ No newline at end of file diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index 980ac9b..97e07dd 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -266,7 +266,7 @@ {% if not receipts %} {% endif %}
    diff --git a/templates/admin/list_products.html b/templates/admin/list_products.html index e7186f3..94aec51 100644 --- a/templates/admin/list_products.html +++ b/templates/admin/list_products.html @@ -53,9 +53,12 @@ {% endfor %} {% if items|length == 0 %} - Pusta lista produktów. + + Pusta lista produktów + {% endif %} +
    diff --git a/templates/admin/mass_edit_categories.html b/templates/admin/mass_edit_categories.html index fff98f5..7ef0f8a 100644 --- a/templates/admin/mass_edit_categories.html +++ b/templates/admin/mass_edit_categories.html @@ -73,6 +73,14 @@ {% endfor %} + {% if l|length == 0 %} + + + Brak list zakupowych do wyświetlenia + + + {% endif %} +
    diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 39c2deb..7b9bbd7 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -65,7 +65,7 @@ {% if not receipts %} {% endif %} diff --git a/templates/admin/user_management.html b/templates/admin/user_management.html index a30c00b..973955a 100644 --- a/templates/admin/user_management.html +++ b/templates/admin/user_management.html @@ -70,7 +70,10 @@ {% else %} ⬇️ Usuń admina {% endif %} - 🗑️ Usuń + + 🗑️ Usuń + {% endfor %} diff --git a/templates/edit_my_list.html b/templates/edit_my_list.html index 81cc2d2..2aea763 100644 --- a/templates/edit_my_list.html +++ b/templates/edit_my_list.html @@ -88,8 +88,8 @@
    - - ❌ Anuluj + + ❌ Anuluj
    @@ -121,14 +121,14 @@ {% endif %} 🔄 Obróć o 90° + class="btn btn-sm btn-outline-light w-100 mb-2">🔄 Obróć o 90° - ✂️ Przytnij - 🗑️ Usuń @@ -140,7 +140,7 @@
    -
    diff --git a/templates/expenses.html b/templates/expenses.html index b6432f0..98ab375 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -123,6 +123,15 @@ {{ '%.2f'|format(list.total_expense) }} {% endfor %} + + {% if list|length == 0 %} + + + Brak list zakupowych do wyświetlenia + + + {% endif %} + From bfcc224a0fedfa04fcc85dd4249f289d9e9e72d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 17:12:51 +0200 Subject: [PATCH 28/90] poprawki i optymalizacje kodu --- app.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app.py b/app.py index 45443ce..1df551e 100644 --- a/app.py +++ b/app.py @@ -2379,20 +2379,27 @@ def admin_panel(): total_expense = db.session.query(func.sum(Expense.amount)).scalar() or 0 avg_list_expense = round(total_expense / list_count, 2) if list_count else 0 + if db.engine.name == "sqlite": + timestamp_diff = func.strftime("%s", Item.purchased_at) - func.strftime("%s", Item.added_at) + elif db.engine.name in ("postgresql", "postgres"): + timestamp_diff = func.extract("epoch", Item.purchased_at) - func.extract("epoch", Item.added_at) + elif db.engine.name in ("mysql", "mariadb"): + timestamp_diff = func.timestampdiff(text("SECOND"), Item.added_at, Item.purchased_at) + else: + timestamp_diff = None + time_to_purchase = ( - db.session.query( - func.avg( - func.strftime("%s", Item.purchased_at) - - func.strftime("%s", Item.added_at) - ) - ) + db.session.query(func.avg(timestamp_diff)) .filter( Item.purchased == True, Item.purchased_at.isnot(None), Item.added_at.isnot(None), ) .scalar() + if timestamp_diff is not None + else None ) + avg_hours_to_purchase = round(time_to_purchase / 3600, 2) if time_to_purchase else 0 first_list = db.session.query(func.min(ShoppingList.created_at)).scalar() From 8d0106c56d77b0c5fafbfd25ab35fe585ce65eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:01:00 +0200 Subject: [PATCH 29/90] fix w /all_products --- app.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 1df551e..2dd4018 100644 --- a/app.py +++ b/app.py @@ -2050,22 +2050,21 @@ def all_products(): .filter(~func.lower(func.trim(SuggestedProduct.name)).in_(used_names)) .all() ) - suggested_fallbacks = [ {"name": row.name.strip(), "count": 0} for row in extra_suggestions ] - if sort == "alphabetical": - products += suggested_fallbacks - products.sort(key=lambda x: x["name"].lower()) - else: - products += suggested_fallbacks + if not products: + products = suggested_fallbacks + if sort == "alphabetical": + products.sort(key=lambda x: x["name"].lower()) return jsonify( - {"products": products, "total_count": total_count + len(suggested_fallbacks)} + {"products": products, "total_count": (total_count if products else len(products))} ) + @app.route("/upload_receipt/", methods=["POST"]) @login_required def upload_receipt(list_id): From a4f82750490040ed6bb6196c9f02f082f9a1f3b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:09:42 +0200 Subject: [PATCH 30/90] zmiany w /all_products, laczenie item i sugested --- app.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index 2dd4018..b106d2e 100644 --- a/app.py +++ b/app.py @@ -45,7 +45,7 @@ from flask_socketio import SocketIO, emit, join_room from config import Config from PIL import Image, ExifTags, ImageFilter, ImageOps from werkzeug.middleware.proxy_fix import ProxyFix -from sqlalchemy import func, extract, inspect, or_, case, text, and_ +from sqlalchemy import func, extract, inspect, or_, case, text, and_, literal from sqlalchemy.orm import joinedload, load_only, aliased from collections import defaultdict, deque from functools import wraps @@ -2008,7 +2008,7 @@ def suggest_products(): return {"suggestions": [s.name for s in suggestions]} -@app.route("/all_products") +""" @app.route("/all_products") def all_products(): sort = request.args.get("sort", "popularity") limit = request.args.get("limit", type=int) or 100 @@ -2061,8 +2061,59 @@ def all_products(): return jsonify( {"products": products, "total_count": (total_count if products else len(products))} + ) """ + +@app.route("/all_products") +def all_products(): + sort = request.args.get("sort", "popularity") + limit = request.args.get("limit", type=int) or 100 + offset = request.args.get("offset", type=int) or 0 + + # Produkty z Item z faktyczną popularnością + products_from_items = ( + db.session.query( + func.lower(func.trim(Item.name)).label("normalized_name"), + func.min(Item.name).label("display_name"), + func.count(func.distinct(Item.list_id)).label("count"), + ) + .group_by("normalized_name") ) + products_from_suggested = ( + db.session.query( + func.lower(func.trim(SuggestedProduct.name)).label("normalized_name"), + func.min(SuggestedProduct.name).label("display_name"), + db.literal(1).label("count"), + ) + .filter(~func.lower(func.trim(SuggestedProduct.name)).in_( + db.session.query(func.lower(func.trim(Item.name))) + )) + .group_by("normalized_name") + ) + + union_q = products_from_items.union_all(products_from_suggested).subquery() + + final_q = ( + db.session.query( + union_q.c.normalized_name, + func.min(union_q.c.display_name).label("display_name"), + func.sum(union_q.c.count).label("count"), + ) + .group_by(union_q.c.normalized_name) + ) + + if sort == "alphabetical": + final_q = final_q.order_by(func.lower(final_q.c.display_name).asc()) + else: + final_q = final_q.order_by(func.sum(union_q.c.count).desc(), func.lower(final_q.c.display_name).asc()) + + total_count = final_q.count() + products = final_q.offset(offset).limit(limit).all() + + out = [{"name": row.display_name, "count": row.count} for row in products] + + return jsonify({"products": out, "total_count": total_count}) + @app.route("/upload_receipt/", methods=["POST"]) From ff0f2a36019a7e24691bdc8fff77c59c30ec1788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:12:31 +0200 Subject: [PATCH 31/90] zmiany w /all_products, laczenie item i sugested --- app.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app.py b/app.py index b106d2e..aff2f6b 100644 --- a/app.py +++ b/app.py @@ -2103,11 +2103,11 @@ def all_products(): ) if sort == "alphabetical": - final_q = final_q.order_by(func.lower(final_q.c.display_name).asc()) + final_q = final_q.order_by(func.lower(union_q.c.display_name).asc()) else: - final_q = final_q.order_by(func.sum(union_q.c.count).desc(), func.lower(final_q.c.display_name).asc()) + final_q = final_q.order_by(func.sum(union_q.c.count).desc(), func.lower(union_q.c.display_name).asc()) - total_count = final_q.count() + total_count = db.session.query(func.count()).select_from(final_q.subquery()).scalar() products = final_q.offset(offset).limit(limit).all() out = [{"name": row.display_name, "count": row.count} for row in products] From f4e10ef20903d67e0f0b1bdeb462e3c2e1f34cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:14:31 +0200 Subject: [PATCH 32/90] zmiany w /all_products, laczenie item i sugested --- app.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index aff2f6b..cd4fe86 100644 --- a/app.py +++ b/app.py @@ -2069,14 +2069,13 @@ def all_products(): limit = request.args.get("limit", type=int) or 100 offset = request.args.get("offset", type=int) or 0 - # Produkty z Item z faktyczną popularnością products_from_items = ( db.session.query( func.lower(func.trim(Item.name)).label("normalized_name"), func.min(Item.name).label("display_name"), func.count(func.distinct(Item.list_id)).label("count"), ) - .group_by("normalized_name") + .group_by(func.lower(func.trim(Item.name))) ) products_from_suggested = ( @@ -2088,7 +2087,7 @@ def all_products(): .filter(~func.lower(func.trim(SuggestedProduct.name)).in_( db.session.query(func.lower(func.trim(Item.name))) )) - .group_by("normalized_name") + .group_by(func.lower(func.trim(SuggestedProduct.name))) ) union_q = products_from_items.union_all(products_from_suggested).subquery() @@ -2096,10 +2095,10 @@ def all_products(): final_q = ( db.session.query( union_q.c.normalized_name, - func.min(union_q.c.display_name).label("display_name"), + union_q.c.display_name, func.sum(union_q.c.count).label("count"), ) - .group_by(union_q.c.normalized_name) + .group_by(union_q.c.normalized_name, union_q.c.display_name) ) if sort == "alphabetical": From ee1a163395bd6ee72a03c94d0df33b37fe061d6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:19:18 +0200 Subject: [PATCH 33/90] poprawki wizualne --- templates/admin/admin_panel.html | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index b13fcc3..2aa3c9a 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -60,6 +60,14 @@ +
    +
    📈 Średnie tempo tworzenia list:
    +
      +
    • 📆 Tygodniowo: {{ avg_per_week }}
    • +
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • +
    • 📅 Rocznie: {{ avg_per_year }}
    • +
    +
    @@ -107,7 +115,7 @@ Miesiąc Rok Całkowite - Średnia + @@ -116,41 +124,32 @@ {{ '%.2f'|format(expense_summary.all.month) }} PLN {{ '%.2f'|format(expense_summary.all.year) }} PLN {{ '%.2f'|format(expense_summary.all.total) }} PLN - {{ '%.2f'|format(expense_summary.all.avg) }} PLN + Aktywne {{ '%.2f'|format(expense_summary.active.month) }} PLN {{ '%.2f'|format(expense_summary.active.year) }} PLN {{ '%.2f'|format(expense_summary.active.total) }} PLN - {{ '%.2f'|format(expense_summary.active.avg) }} PLN + Archiwalne {{ '%.2f'|format(expense_summary.archived.month) }} PLN {{ '%.2f'|format(expense_summary.archived.year) }} PLN {{ '%.2f'|format(expense_summary.archived.total) }} PLN - {{ '%.2f'|format(expense_summary.archived.avg) }} PLN + Wygasłe {{ '%.2f'|format(expense_summary.expired.month) }} PLN {{ '%.2f'|format(expense_summary.expired.year) }} PLN {{ '%.2f'|format(expense_summary.expired.total) }} PLN - {{ '%.2f'|format(expense_summary.expired.avg) }} PLN + -
    -
    📈 Średnie tempo tworzenia list:
    -
      -
    • 📆 Tygodniowo: {{ avg_per_week }}
    • -
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • -
    • 📅 Rocznie: {{ avg_per_year }}
    • -
    -
    - 📊 Pokaż wykres wydatków From 32f491f978317f6e96acc8310c5849487b60fed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:20:48 +0200 Subject: [PATCH 34/90] poprawki wizualne --- templates/admin/admin_panel.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 2aa3c9a..da84a4f 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -60,15 +60,17 @@ -
    -
    📈 Średnie tempo tworzenia list:
    + +
    + +
    +
    📈 Średnie tempo tworzenia list:
    • 📆 Tygodniowo: {{ avg_per_week }}
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • 📅 Rocznie: {{ avg_per_year }}
    - From 87000bf90c0a9810e46de111945bcb8413144ab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:23:23 +0200 Subject: [PATCH 35/90] poprawki wizualne --- templates/admin/admin_panel.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index da84a4f..f8fb571 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -60,17 +60,16 @@ - -
    -
    📈 Średnie tempo tworzenia list:
    +
    📈 Średnie tempo tworzenia list:
    • 📆 Tygodniowo: {{ avg_per_week }}
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • 📅 Rocznie: {{ avg_per_year }}
    -
    + + From b4f1e43f5f3714c7ae13b18379421d58d69f4a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:24:38 +0200 Subject: [PATCH 36/90] poprawki wizualne --- templates/admin/admin_panel.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index f8fb571..66114b4 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -60,16 +60,14 @@ - -
    📈 Średnie tempo tworzenia list:
    +
    📈 Średnie tempo tworzenia list:
    • 📆 Tygodniowo: {{ avg_per_week }}
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • 📅 Rocznie: {{ avg_per_year }}
    - - + From 268f8d2e8509b761326168a3405d50288a7b9d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 18:25:49 +0200 Subject: [PATCH 37/90] poprawki wizualne --- templates/admin/admin_panel.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index 66114b4..f51dd52 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -60,12 +60,12 @@ - +
    📈 Średnie tempo tworzenia list:
    • 📆 Tygodniowo: {{ avg_per_week }}
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • -
    • 📅 Rocznie: {{ avg_per_year }}
    • +
    From dd65230636338e61d9144921c9184f21c4080f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 22:56:25 +0200 Subject: [PATCH 38/90] wyszukiwanie i dodawanie sugestii oraz poprawki --- app.py | 22 +++++++++++++++++++++ static/js/product_suggestion.js | 2 +- static/js/table_search.js | 28 +++++++++++++++++++++++++++ templates/admin/list_products.html | 31 +++++++++++++++++++++++++++++- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 static/js/table_search.js diff --git a/app.py b/app.py index cd4fe86..1f23136 100644 --- a/app.py +++ b/app.py @@ -3276,6 +3276,28 @@ def admin_list_items_json(list_id): ) +@app.route("/admin/add_suggestion", methods=["POST"]) +@login_required +@admin_required +def add_suggestion(): + name = request.form.get("suggestion_name", "").strip() + + if not name: + flash("Nazwa nie może być pusta", "warning") + return redirect(url_for("list_products")) + + existing = db.session.query(SuggestedProduct).filter_by(name=name).first() + if existing: + flash("Sugestia już istnieje", "warning") + else: + new_suggestion = SuggestedProduct(name=name) + db.session.add(new_suggestion) + db.session.commit() + flash("Dodano sugestię", "success") + + return redirect(url_for("list_products")) + + @app.route("/healthcheck") def healthcheck(): header_token = request.headers.get("X-Internal-Check") diff --git a/static/js/product_suggestion.js b/static/js/product_suggestion.js index 0c1af4c..4d3b0c8 100644 --- a/static/js/product_suggestion.js +++ b/static/js/product_suggestion.js @@ -66,7 +66,7 @@ function bindDeleteButton(button) { const syncBtn = cell.querySelector('.sync-btn'); if (syncBtn) bindSyncButton(syncBtn); } else { - cell.innerHTML = 'Usunięto synchronizacje'; + cell.innerHTML = 'Usunięto z bazy danych'; } }) .catch(() => { diff --git a/static/js/table_search.js b/static/js/table_search.js new file mode 100644 index 0000000..2958ba2 --- /dev/null +++ b/static/js/table_search.js @@ -0,0 +1,28 @@ +document.addEventListener("DOMContentLoaded", function () { + const searchInput = document.getElementById("search-table"); + const clearButton = document.getElementById("clear-search"); + const rows = document.querySelectorAll("table tbody tr"); + + if (!searchInput || !rows.length) return; + + function filterTable(query) { + const q = query.toLowerCase(); + + rows.forEach(row => { + const rowText = row.textContent.toLowerCase(); + row.style.display = rowText.includes(q) ? "" : "none"; + }); + } + + searchInput.addEventListener("input", function () { + filterTable(this.value); + }); + + if (clearButton) { + clearButton.addEventListener("click", function () { + searchInput.value = ""; + filterTable(""); // Pokaż wszystko + searchInput.focus(); + }); + } +}); \ No newline at end of file diff --git a/templates/admin/list_products.html b/templates/admin/list_products.html index 94aec51..3871da0 100644 --- a/templates/admin/list_products.html +++ b/templates/admin/list_products.html @@ -7,6 +7,32 @@ ← Powrót do panelu +
    +
    + + +
    + +
    + + +
    +
    + +
    + + + +
    + + +
    + +
    +
    +
    @@ -96,7 +122,9 @@ {% endfor %} {% if orphan_suggestions|length == 0 %} - Brak sugestii do wyświetlenia. + + Brak niepowiązanych sugestii do wyświetlenia + {% endif %} @@ -135,6 +163,7 @@ {% block scripts %} + {% endblock %} {% endblock %} \ No newline at end of file From 35d99825421faf4e2d034d8f99546e8226ba0eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 22:58:04 +0200 Subject: [PATCH 39/90] wyszukiwanie i dodawanie sugestii oraz poprawki --- templates/expenses.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/expenses.html b/templates/expenses.html index 98ab375..94abfa1 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -71,7 +71,7 @@ id="customStart"> Do - +
    @@ -169,7 +169,7 @@ Do - +
    From 04995f4ab4bbd3c37a0cca8e114bd863a4e2b362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sun, 17 Aug 2025 23:33:28 +0200 Subject: [PATCH 40/90] poprawka w warunku --- templates/admin/mass_edit_categories.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/mass_edit_categories.html b/templates/admin/mass_edit_categories.html index 7ef0f8a..bd68283 100644 --- a/templates/admin/mass_edit_categories.html +++ b/templates/admin/mass_edit_categories.html @@ -73,7 +73,7 @@ {% endfor %} - {% if l|length == 0 %} + {% if lists|length == 0 %} Brak list zakupowych do wyświetlenia From 5d977c644b1edb7c185ca0df22872daf5d43b4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 00:16:26 +0200 Subject: [PATCH 41/90] w wydatkach domyslnie tylko z wydatkami >0 --- static/js/expense_table.js | 21 +-------------------- templates/expenses.html | 18 ------------------ 2 files changed, 1 insertion(+), 38 deletions(-) diff --git a/static/js/expense_table.js b/static/js/expense_table.js index c8971cc..35de2ba 100644 --- a/static/js/expense_table.js +++ b/static/js/expense_table.js @@ -4,10 +4,9 @@ document.addEventListener('DOMContentLoaded', () => { const filterButtons = document.querySelectorAll('.range-btn'); const rows = document.querySelectorAll('#listsTableBody tr'); const categoryButtons = document.querySelectorAll('.category-filter'); - const onlyWith = document.getElementById('onlyWithExpenses'); window.selectedCategoryId = ""; - let initialLoad = true; // flaga - true tylko przy pierwszym wejściu + let initialLoad = true; function updateTotal() { let total = 0; @@ -74,7 +73,6 @@ document.addEventListener('DOMContentLoaded', () => { } function applyExpenseFilter() { - if (!onlyWith || !onlyWith.checked) return; rows.forEach(row => { const amt = parseFloat(row.querySelector('.list-checkbox').dataset.amount || 0); if (amt <= 0) row.style.display = 'none'; @@ -121,23 +119,6 @@ document.addEventListener('DOMContentLoaded', () => { }); }); - // Checkbox "tylko z wydatkami" - if (onlyWith) { - onlyWith.addEventListener('change', () => { - if (initialLoad) { - filterByLast30Days(); - } else { - const activeRange = document.querySelector('.range-btn.active'); - if (activeRange) { - filterByRange(activeRange.dataset.range); - } - } - applyExpenseFilter(); - applyCategoryFilter(); - updateTotal(); - }); - } - // Obsługa kliknięcia w kategorię categoryButtons.forEach(btn => { btn.addEventListener('click', () => { diff --git a/templates/expenses.html b/templates/expenses.html index 94abfa1..b7d2765 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -75,15 +75,6 @@ -
    -
    - - -
    -
    -
    @@ -123,15 +114,6 @@ {{ '%.2f'|format(list.total_expense) }} {% endfor %} - - {% if list|length == 0 %} - - - Brak list zakupowych do wyświetlenia - - - {% endif %} -
    From 7762cba5417ca6bc00d2dc4e501a32c7a936015d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 00:40:25 +0200 Subject: [PATCH 42/90] poprawka w suwaku --- static/js/show_all_expense.js | 17 +++++++++++++++++ templates/expenses.html | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 static/js/show_all_expense.js diff --git a/static/js/show_all_expense.js b/static/js/show_all_expense.js new file mode 100644 index 0000000..eba67b6 --- /dev/null +++ b/static/js/show_all_expense.js @@ -0,0 +1,17 @@ +document.addEventListener('DOMContentLoaded', function () { + const showAllCheckbox = document.getElementById('showAllLists'); + if (!showAllCheckbox) return; + + const params = new URLSearchParams(window.location.search); + if (!params.has('show_all')) { + params.set('show_all', 'true'); + window.history.replaceState({}, '', `${window.location.pathname}?${params.toString()}`); + } + showAllCheckbox.checked = params.get('show_all') === 'true'; + + showAllCheckbox.addEventListener('change', function () { + const urlParams = new URLSearchParams(window.location.search); + urlParams.set('show_all', showAllCheckbox.checked ? 'true' : 'false'); + window.location.search = urlParams.toString(); + }); +}); diff --git a/templates/expenses.html b/templates/expenses.html index b7d2765..d27e262 100644 --- a/templates/expenses.html +++ b/templates/expenses.html @@ -163,9 +163,9 @@ {% block scripts %} + - {% endblock %} \ No newline at end of file From 95cc506abf0e71c420b8bfe743635e09fe02d2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 00:48:16 +0200 Subject: [PATCH 43/90] poprawka w suwaku --- static/js/expense_chart.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/js/expense_chart.js b/static/js/expense_chart.js index 77f830d..58d69cd 100644 --- a/static/js/expense_chart.js +++ b/static/js/expense_chart.js @@ -9,10 +9,14 @@ document.addEventListener("DOMContentLoaded", function () { function loadExpenses(range = "last30days", startDate = null, endDate = null) { let url = '/expenses_data?range=' + range; + const showAllCheckbox = document.getElementById("showAllLists"); - if (showAllCheckbox && showAllCheckbox.checked) { + if (showAllCheckbox) { + url += showAllCheckbox.checked ? '&show_all=true' : '&show_all=false'; + } else { url += '&show_all=true'; - } + } + if (startDate && endDate) { url += `&start_date=${startDate}&end_date=${endDate}`; } From 92c257abfce872fd4e1775be747f288e69f59e83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 00:51:09 +0200 Subject: [PATCH 44/90] sortowanie userow --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 1f23136..cd087e6 100644 --- a/app.py +++ b/app.py @@ -2557,7 +2557,7 @@ def add_user(): @login_required @admin_required def list_users(): - users = User.query.all() + users = User.query.order_by(User.id.asc()).all() user_data = [] for user in users: From f9ffd083afebb53c4307c9893bf0d0e9349243a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 00:53:50 +0200 Subject: [PATCH 45/90] poprawki wizualne --- templates/admin/edit_list.html | 2 +- templates/admin/user_management.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/admin/edit_list.html b/templates/admin/edit_list.html index 97e07dd..e01349b 100644 --- a/templates/admin/edit_list.html +++ b/templates/admin/edit_list.html @@ -138,7 +138,7 @@ value="1">
    - +
    diff --git a/templates/admin/user_management.html b/templates/admin/user_management.html index 973955a..9f62302 100644 --- a/templates/admin/user_management.html +++ b/templates/admin/user_management.html @@ -22,7 +22,7 @@ placeholder="Hasło" required>
    - +
    From 899bb6eb3a80a1265d41e248c73e8f49fc551d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 10:18:40 +0200 Subject: [PATCH 46/90] zmniejszenie jakosci wgrywanych zjec --- app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index cd087e6..e14a04a 100644 --- a/app.py +++ b/app.py @@ -142,9 +142,9 @@ TIME_WINDOW = 60 * 60 WEBP_SAVE_PARAMS = { "format": "WEBP", - "lossless": True, # lub False jeśli chcesz używać quality + "lossless": False, # False jeśli chcesz używać quality "method": 6, - # "quality": 95, # tylko jeśli lossless=False + "quality": 95, # tylko jeśli lossless=False } db = SQLAlchemy(app) @@ -500,7 +500,7 @@ def save_resized_image(file, path): pass try: - image.thumbnail((2000, 2000)) + image.thumbnail((1500, 1500)) image = image.convert("RGB") image.info.clear() @@ -965,9 +965,9 @@ def save_pdf_as_webp(file, path): combined.paste(img, (0, y_offset)) y_offset += img.height - combined.thumbnail((2000, 20000)) new_path = path.rsplit(".", 1)[0] + ".webp" - combined.save(new_path, **WEBP_SAVE_PARAMS) + #combined.save(new_path, **WEBP_SAVE_PARAMS) + combined.save(new_path, format="WEBP") except Exception as e: raise ValueError(f"Błąd podczas przetwarzania PDF: {e}") From 3cddb79e4fb52d19c4aea7edaa425c9d0716a77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 10:26:12 +0200 Subject: [PATCH 47/90] fix typo --- templates/admin/user_management.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/admin/user_management.html b/templates/admin/user_management.html index 9f62302..91a6703 100644 --- a/templates/admin/user_management.html +++ b/templates/admin/user_management.html @@ -22,7 +22,7 @@ placeholder="Hasło" required>
    - +
    From 8b1057d82411b887ffce21648a3da95d7553fd1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 10:28:26 +0200 Subject: [PATCH 48/90] poprawka wizualna --- templates/admin/user_management.html | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/templates/admin/user_management.html b/templates/admin/user_management.html index 91a6703..86f4074 100644 --- a/templates/admin/user_management.html +++ b/templates/admin/user_management.html @@ -70,10 +70,17 @@ {% else %} ⬇️ Usuń admina {% endif %} - - 🗑️ Usuń - + {% if user.username == 'admin' %} + + 🗑️ Usuń + + {% else %} + + 🗑️ Usuń + + {% endif %} {% endfor %} From fc108bceb5d1821fde8e9a1d856f77bb112787f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 18 Aug 2025 14:08:33 +0200 Subject: [PATCH 49/90] zmiany ux oraz nowe funkcje --- app.py | 24 +++++ templates/admin/admin_panel.html | 120 +++++++++++----------- templates/admin/list_products.html | 1 + templates/admin/mass_edit_categories.html | 8 +- templates/admin/receipts.html | 37 ++++++- templates/admin/user_management.html | 43 ++++---- templates/edit_my_list.html | 6 +- 7 files changed, 149 insertions(+), 90 deletions(-) diff --git a/app.py b/app.py index e14a04a..d7d860a 100644 --- a/app.py +++ b/app.py @@ -1748,6 +1748,18 @@ def create_list(): @app.route("/list/") @login_required def view_list(list_id): + + shopping_list = db.session.get(ShoppingList, list_id) + if not shopping_list: + abort(404) + + is_owner = current_user.id == shopping_list.owner_id + if not is_owner: + flash("Nie jesteś właścicielem listy, przekierowano do widoku publicznego.", "warning") + if current_user.is_admin: + flash("W celu modyfikacji listy, przejdź do panelu administracyjnego.", "info") + return redirect(url_for("shared_list", token=shopping_list.share_token)) + shopping_list, items, receipt_files, expenses, total_expense = get_list_details( list_id ) @@ -2541,6 +2553,10 @@ def add_user(): flash("Wypełnij wszystkie pola", "danger") return redirect(url_for("list_users")) + if len(password) < 6: + flash("Hasło musi mieć co najmniej 6 znaków", "danger") + return redirect(url_for("list_users")) + if User.query.filter(func.lower(User.username) == username).first(): flash("Użytkownik o takiej nazwie już istnieje", "warning") return redirect(url_for("list_users")) @@ -2689,6 +2705,12 @@ def admin_receipts(id): flash("Nieprawidłowe ID listy.", "danger") return redirect(url_for("admin_panel")) + total_filesize = ( + db.session.query(func.sum(Receipt.filesize)).scalar() or 0 + ) + + page_filesize = sum(r.filesize or 0 for r in receipts_paginated) + query_string = urlencode({k: v for k, v in request.args.items() if k != "page"}) return render_template( @@ -2701,6 +2723,8 @@ def admin_receipts(id): total_pages=total_pages, id=id, query_string=query_string, + total_filesize=total_filesize, + page_filesize=page_filesize, ) diff --git a/templates/admin/admin_panel.html b/templates/admin/admin_panel.html index f51dd52..90835f5 100644 --- a/templates/admin/admin_panel.html +++ b/templates/admin/admin_panel.html @@ -62,11 +62,11 @@
    📈 Średnie tempo tworzenia list:
    -
      -
    • 📆 Tygodniowo: {{ avg_per_week }}
    • -
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • - -
    +
      +
    • 📆 Tygodniowo: {{ avg_per_week }}
    • +
    • 🗓️ Miesięcznie: {{ avg_per_month }}
    • + +
    @@ -114,7 +114,7 @@ Miesiąc Rok Całkowite - + @@ -123,21 +123,21 @@ {{ '%.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 @@ -156,59 +156,59 @@ - - {# panel wyboru miesiąca zawsze widoczny #} -
    - - {# LEWA STRONA — przyciski ← → TYLKO gdy nie show_all #} -
    - {% if not show_all %} - {% set current_date = now.replace(day=1) %} - {% set prev_month = (current_date - timedelta(days=1)).strftime('%Y-%m') %} - {% set next_month = (current_date + timedelta(days=31)).replace(day=1).strftime('%Y-%m') %} - - {% if prev_month in month_options %} - - ← {{ prev_month }} - - {% else %} - - {% endif %} - - {% if next_month in month_options %} - - {{ next_month }} → - - {% else %} - - {% endif %} - {% else %} - {# Tryb wszystkie miesiące — możemy pokazać skrót do bieżącego miesiąca #} - - 📅 Przejdź do bieżącego miesiąca - - {% endif %} -
    - - {# PRAWA STRONA — picker miesięcy zawsze widoczny #} -
    -
    - 📅 - -
    -
    -
    -
    + + {# panel wyboru miesiąca zawsze widoczny #} +
    + + {# LEWA STRONA — przyciski ← → TYLKO gdy nie show_all #} +
    + {% if not show_all %} + {% set current_date = now.replace(day=1) %} + {% set prev_month = (current_date - timedelta(days=1)).strftime('%Y-%m') %} + {% set next_month = (current_date + timedelta(days=31)).replace(day=1).strftime('%Y-%m') %} + + {% if prev_month in month_options %} + + ← {{ prev_month }} + + {% else %} + + {% endif %} + + {% if next_month in month_options %} + + {{ next_month }} → + + {% else %} + + {% endif %} + {% else %} + {# Tryb wszystkie miesiące — możemy pokazać skrót do bieżącego miesiąca #} + + 📅 Przejdź do bieżącego miesiąca + + {% endif %} +
    + + {# PRAWA STRONA — picker miesięcy zawsze widoczny #} +
    +
    + 📅 + +
    +
    +
    +

    📄 Listy zakupowe {% if show_all %} diff --git a/templates/admin/list_products.html b/templates/admin/list_products.html index 3871da0..6637b75 100644 --- a/templates/admin/list_products.html +++ b/templates/admin/list_products.html @@ -132,6 +132,7 @@

    +
    diff --git a/templates/admin/mass_edit_categories.html b/templates/admin/mass_edit_categories.html index bd68283..6daab00 100644 --- a/templates/admin/mass_edit_categories.html +++ b/templates/admin/mass_edit_categories.html @@ -80,18 +80,16 @@ {% endif %} -
    - -
    - +
    +
    - +
    diff --git a/templates/admin/receipts.html b/templates/admin/receipts.html index 7b9bbd7..9198390 100644 --- a/templates/admin/receipts.html +++ b/templates/admin/receipts.html @@ -3,7 +3,35 @@ {% block content %}
    -

    📸 Wszystkie paragony

    +

    + 📸 {% if id == 'all' %}Wszystkie paragony{% else %}Paragony dla listy #{{ id }}{% endif %} +

    + +

    + {% if id == 'all' %} + Rozmiar plików tej strony: + {% else %} + Rozmiar plików listy #{{ id }}: + {% endif %} + + {% if page_filesize >= 1024*1024 %} + {{ (page_filesize / 1024 / 1024) | round(2) }} MB + {% else %} + {{ (page_filesize / 1024) | round(1) }} kB + {% endif %} + + | + Łącznie: + + {% if total_filesize >= 1024*1024 %} + {{ (total_filesize / 1024 / 1024) | round(2) }} MB + {% else %} + {{ (total_filesize / 1024) | round(1) }} kB + {% endif %} + +

    + +
    +
    @@ -137,8 +166,10 @@