paginacja i poprawki uxowe
This commit is contained in:
103
app.py
103
app.py
@@ -2447,21 +2447,21 @@ def delete_user(user_id):
|
||||
@admin_required
|
||||
def admin_receipts(id):
|
||||
try:
|
||||
page = request.args.get("page", 1, type=int)
|
||||
per_page = request.args.get("per_page", 24, type=int)
|
||||
per_page = max(1, min(per_page, 200)) # sanity check
|
||||
|
||||
if id == "all":
|
||||
all_filenames = {r.filename for r in Receipt.query.all()}
|
||||
|
||||
per_page = 24
|
||||
page = request.args.get("page", 1, type=int)
|
||||
total_count = Receipt.query.count()
|
||||
total_pages = (total_count + per_page - 1) // per_page
|
||||
|
||||
receipts_paginated = (
|
||||
pagination = (
|
||||
Receipt.query.order_by(Receipt.uploaded_at.desc())
|
||||
.offset((page - 1) * per_page)
|
||||
.limit(per_page)
|
||||
.all()
|
||||
.paginate(page=page, per_page=per_page, error_out=False)
|
||||
)
|
||||
|
||||
receipts_paginated = pagination.items
|
||||
total_pages = pagination.pages
|
||||
|
||||
upload_folder = app.config["UPLOAD_FOLDER"]
|
||||
files_on_disk = set(os.listdir(upload_folder))
|
||||
orphan_files = [
|
||||
@@ -2471,7 +2471,6 @@ def admin_receipts(id):
|
||||
and f not in all_filenames
|
||||
and f.startswith("list_")
|
||||
]
|
||||
|
||||
else:
|
||||
list_id = int(id)
|
||||
receipts_paginated = (
|
||||
@@ -2482,13 +2481,12 @@ def admin_receipts(id):
|
||||
orphan_files = []
|
||||
page = 1
|
||||
total_pages = 1
|
||||
per_page = len(receipts_paginated) or 1
|
||||
except ValueError:
|
||||
flash("Nieprawidłowe ID listy.", "danger")
|
||||
return redirect(url_for("admin_panel"))
|
||||
|
||||
args_without_page = request.args.to_dict()
|
||||
args_without_page.pop("page", None)
|
||||
query_string = urlencode(args_without_page)
|
||||
query_string = urlencode({k: v for k, v in request.args.items() if k != "page"})
|
||||
|
||||
return render_template(
|
||||
"admin/receipts.html",
|
||||
@@ -2496,6 +2494,7 @@ def admin_receipts(id):
|
||||
orphan_files=orphan_files,
|
||||
orphan_files_count=len(orphan_files),
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=total_pages,
|
||||
id=id,
|
||||
query_string=query_string,
|
||||
@@ -2834,10 +2833,17 @@ def edit_list(list_id):
|
||||
@login_required
|
||||
@admin_required
|
||||
def list_products():
|
||||
items = Item.query.options(
|
||||
page = request.args.get("page", 1, type=int)
|
||||
per_page = request.args.get("per_page", 125, type=int)
|
||||
per_page = max(1, min(per_page, 300))
|
||||
|
||||
items_query = Item.query.options(
|
||||
joinedload(Item.shopping_list),
|
||||
joinedload(Item.added_by_user),
|
||||
).order_by(Item.id.desc()).all()
|
||||
).order_by(Item.id.desc())
|
||||
|
||||
pagination = items_query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
items = pagination.items
|
||||
|
||||
users = User.query.all()
|
||||
users_dict = {user.id: user.username for user in users}
|
||||
@@ -2847,19 +2853,7 @@ def list_products():
|
||||
(s.name or "").strip().lower(): s for s in suggestions if s.name and s.name.strip()
|
||||
}
|
||||
|
||||
seen_names = set()
|
||||
unique_items = []
|
||||
used_suggestion_names = set()
|
||||
|
||||
for item in items:
|
||||
normalized_name = (item.name or "").strip().lower()
|
||||
if not normalized_name:
|
||||
continue
|
||||
if normalized_name not in seen_names:
|
||||
seen_names.add(normalized_name)
|
||||
unique_items.append(item)
|
||||
used_suggestion_names.add(normalized_name)
|
||||
|
||||
used_suggestion_names = {(item.name or "").strip().lower() for item in items if item.name}
|
||||
orphan_suggestions = [
|
||||
s for s in suggestions
|
||||
if (s.name or "").strip().lower() not in used_suggestion_names and (s.name or "").strip()
|
||||
@@ -2871,12 +2865,18 @@ def list_products():
|
||||
if name in all_suggestions_dict
|
||||
}
|
||||
|
||||
query_string = urlencode({k: v for k, v in request.args.items() if k != "page"})
|
||||
|
||||
return render_template(
|
||||
"admin/list_products.html",
|
||||
items=unique_items,
|
||||
items=items,
|
||||
users_dict=users_dict,
|
||||
suggestions_dict=suggestions_dict,
|
||||
orphan_suggestions=orphan_suggestions,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=pagination.pages,
|
||||
query_string=query_string,
|
||||
)
|
||||
|
||||
|
||||
@@ -2981,19 +2981,23 @@ def recalculate_filesizes_all():
|
||||
return redirect(url_for("admin_receipts", id="all"))
|
||||
|
||||
|
||||
|
||||
@app.route("/admin/mass_edit_categories", methods=["GET", "POST"])
|
||||
@login_required
|
||||
@admin_required
|
||||
def admin_mass_edit_categories():
|
||||
lists = (
|
||||
ShoppingList.query.options(
|
||||
joinedload(ShoppingList.categories),
|
||||
joinedload(ShoppingList.items),
|
||||
joinedload(ShoppingList.owner),
|
||||
)
|
||||
.order_by(ShoppingList.created_at.desc())
|
||||
.all()
|
||||
)
|
||||
page = request.args.get("page", 1, type=int)
|
||||
per_page = request.args.get("per_page", 50, type=int)
|
||||
per_page = max(1, min(per_page, 200)) # ogranicz do sensownych wartości
|
||||
|
||||
lists_query = ShoppingList.query.options(
|
||||
joinedload(ShoppingList.categories),
|
||||
joinedload(ShoppingList.items),
|
||||
joinedload(ShoppingList.owner),
|
||||
).order_by(ShoppingList.created_at.desc())
|
||||
|
||||
pagination = lists_query.paginate(page=page, per_page=per_page, error_out=False)
|
||||
lists = pagination.items
|
||||
|
||||
categories = Category.query.order_by(Category.name.asc()).all()
|
||||
|
||||
@@ -3011,12 +3015,19 @@ def admin_mass_edit_categories():
|
||||
l.categories.extend(cats)
|
||||
db.session.commit()
|
||||
flash("Zaktualizowano kategorie dla wybranych list", "success")
|
||||
return redirect(url_for("admin_mass_edit_categories"))
|
||||
return redirect(url_for("admin_mass_edit_categories", page=page, per_page=per_page))
|
||||
|
||||
query_string = urlencode({k: v for k, v in request.args.items() if k != "page"})
|
||||
|
||||
return render_template(
|
||||
"admin/mass_edit_categories.html",
|
||||
lists=lists,
|
||||
categories=categories
|
||||
categories=categories,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=pagination.pages,
|
||||
total_items=pagination.total,
|
||||
query_string=query_string,
|
||||
)
|
||||
|
||||
|
||||
@@ -3038,7 +3049,17 @@ def admin_list_items_json(list_id):
|
||||
for item in l.items
|
||||
]
|
||||
|
||||
return jsonify({"title": l.title, "items": items})
|
||||
purchased_count = sum(1 for item in l.items if item.purchased)
|
||||
total_expense = sum(exp.amount for exp in l.expenses)
|
||||
|
||||
return jsonify({
|
||||
"title": l.title,
|
||||
"items": items,
|
||||
"total_count": len(l.items),
|
||||
"purchased_count": purchased_count,
|
||||
"total_expense": round(total_expense, 2)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@app.route("/healthcheck")
|
||||
|
@@ -2,10 +2,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const modalElement = document.getElementById("productPreviewModal");
|
||||
const modal = new bootstrap.Modal(modalElement);
|
||||
|
||||
modalElement.addEventListener('hidden.bs.modal', function () {
|
||||
document.querySelectorAll('.modal-backdrop').forEach(el => el.remove());
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.overflow = '';
|
||||
modalElement.addEventListener("hidden.bs.modal", function () {
|
||||
document.querySelectorAll(".modal-backdrop").forEach((el) => el.remove());
|
||||
document.body.classList.remove("modal-open");
|
||||
document.body.style.overflow = "";
|
||||
});
|
||||
|
||||
document.querySelectorAll(".preview-btn").forEach((btn) => {
|
||||
@@ -15,7 +15,10 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const productList = document.getElementById("product-list");
|
||||
|
||||
modalTitle.textContent = "Ładowanie...";
|
||||
productList.innerHTML = '<li class="list-group-item bg-dark text-white">⏳ Ładowanie produktów...</li>';
|
||||
productList.innerHTML = `
|
||||
<li class="list-group-item bg-dark text-white">
|
||||
⏳ Ładowanie produktów...
|
||||
</li>`;
|
||||
|
||||
modal.show();
|
||||
|
||||
@@ -26,6 +29,24 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
modalTitle.textContent = `🛒 ${data.title}`;
|
||||
productList.innerHTML = "";
|
||||
|
||||
// 🔢 PODSUMOWANIE
|
||||
const summary = document.createElement("div");
|
||||
summary.className = "mb-3";
|
||||
|
||||
const percent =
|
||||
data.total_count > 0
|
||||
? Math.round((data.purchased_count / data.total_count) * 100)
|
||||
: 0;
|
||||
|
||||
summary.innerHTML = `
|
||||
<p class="mb-1">📦 <strong>${data.total_count}</strong> produktów</p>
|
||||
<p class="mb-1">✅ Kupione: <strong>${data.purchased_count}</strong> (${percent}%)</p>
|
||||
<p class="mb-1">💸 Wydatek: <strong>${data.total_expense.toFixed(2)} zł</strong></p>
|
||||
<hr class="my-2">
|
||||
`;
|
||||
productList.appendChild(summary);
|
||||
|
||||
// 🛒 LISTY PRODUKTÓW
|
||||
const purchasedList = document.createElement("ul");
|
||||
purchasedList.className = "list-group list-group-flush mb-3";
|
||||
|
||||
@@ -35,14 +56,20 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
let hasPurchased = false;
|
||||
let hasUnpurchased = false;
|
||||
|
||||
data.items.forEach(item => {
|
||||
data.items.forEach((item) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "list-group-item bg-dark text-white d-flex justify-content-between";
|
||||
li.className =
|
||||
"list-group-item bg-dark text-white d-flex justify-content-between";
|
||||
li.innerHTML = `
|
||||
<span>${item.name}</span>
|
||||
<span class="badge ${item.purchased ? 'bg-success' : item.not_purchased ? 'bg-warning text-dark' : 'bg-secondary'}">
|
||||
x${item.quantity}
|
||||
</span>`;
|
||||
<span>${item.name}</span>
|
||||
<span class="badge ${item.purchased
|
||||
? "bg-success"
|
||||
: item.not_purchased
|
||||
? "bg-warning text-dark"
|
||||
: "bg-secondary"
|
||||
}">
|
||||
x${item.quantity}
|
||||
</span>`;
|
||||
|
||||
if (item.purchased) {
|
||||
purchasedList.appendChild(li);
|
||||
@@ -68,13 +95,18 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
}
|
||||
|
||||
if (!hasPurchased && !hasUnpurchased) {
|
||||
productList.innerHTML = '<li class="list-group-item bg-dark text-muted fst-italic">Brak produktów</li>';
|
||||
productList.innerHTML = `
|
||||
<li class="list-group-item bg-dark text-muted fst-italic">
|
||||
Brak produktów
|
||||
</li>`;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
modalTitle.textContent = "Błąd";
|
||||
productList.innerHTML = '<li class="list-group-item bg-dark text-danger">❌ Błąd podczas ładowania</li>';
|
||||
productList.innerHTML = `
|
||||
<li class="list-group-item bg-dark text-danger">
|
||||
❌ Błąd podczas ładowania
|
||||
</li>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🛠️ Edytuj listę #{{ list.id }}</h2>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót</a>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">🛍️ Produkty i sugestie</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<div class="card bg-dark text-white mb-5">
|
||||
@@ -16,7 +16,7 @@
|
||||
<span class="badge bg-info">{{ items|length }} produktów</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<table class="table table-dark table-striped align-middle m-0">
|
||||
<table class="table table-dark table-striped align-middle sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% set item_names = items | map(attribute='name') | map('lower') | list %}
|
||||
<table class="table table-dark table-striped align-middle m-0">
|
||||
<table class="table table-dark table-striped align-middle sortable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
@@ -117,6 +117,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<form method="get" class="d-flex align-items-center">
|
||||
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
|
||||
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
|
||||
<option value="25" {% if per_page==100 %}selected{% endif %}>100</option>
|
||||
<option value="50" {% if per_page==200 %}selected{% endif %}>200</option>
|
||||
<option value="100" {% if per_page==300 %}selected{% endif %}>300</option>
|
||||
</select>
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
</form>
|
||||
|
||||
<nav aria-label="Nawigacja stron">
|
||||
<ul class="pagination pagination-dark mb-0">
|
||||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page - 1 }}">«</a>
|
||||
</li>
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page + 1 }}">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Modal podglądu produktów -->
|
||||
<div class="modal fade" id="productPreviewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
|
@@ -81,6 +81,37 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<form method="get" class="d-flex align-items-center">
|
||||
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
|
||||
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
|
||||
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
|
||||
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
|
||||
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
|
||||
</select>
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
</form>
|
||||
|
||||
<nav aria-label="Nawigacja stron">
|
||||
<ul class="pagination pagination-dark mb-0">
|
||||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page - 1 }}">«</a>
|
||||
</li>
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page + 1 }}">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal podglądu produktów -->
|
||||
<div class="modal fade" id="productPreviewModal" tabindex="-1" aria-labelledby="previewModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<a href="{{ url_for('recalculate_filesizes_all') }}" class="btn btn-outline-primary me-2">
|
||||
Przelicz rozmiary plików
|
||||
</a>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,36 +66,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Nawigacja stron">
|
||||
<ul class="pagination pagination-dark justify-content-center">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4">
|
||||
<form method="get" class="d-flex align-items-center">
|
||||
<label for="per_page" class="me-2">🔢 Pozycji na stronę:</label>
|
||||
<select id="per_page" name="per_page" class="form-select form-select-sm me-2" onchange="this.form.submit()">
|
||||
<option value="25" {% if per_page==25 %}selected{% endif %}>25</option>
|
||||
<option value="50" {% if per_page==50 %}selected{% endif %}>50</option>
|
||||
<option value="100" {% if per_page==100 %}selected{% endif %}>100</option>
|
||||
</select>
|
||||
<input type="hidden" name="page" value="{{ page }}">
|
||||
</form>
|
||||
|
||||
{# Poprzednia strona #}
|
||||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('admin_receipts', id=id) }}?{{ query_string }}{% if query_string %}&{% endif %}page={{ page-1 }}">
|
||||
« Poprzednia
|
||||
</a>
|
||||
</li>
|
||||
<nav aria-label="Nawigacja stron">
|
||||
<ul class="pagination pagination-dark mb-0">
|
||||
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page - 1 }}">«</a>
|
||||
</li>
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">{{ p }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
<a class="page-link" href="?{{ query_string }}{% if query_string %}&{% endif %}page={{ page + 1 }}">»</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{# Numery stron #}
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
<li class="page-item {% if p == page %}active{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('admin_receipts', id=id) }}?{{ query_string }}{% if query_string %}&{% endif %}page={{ p }}">
|
||||
{{ p }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{# Następna strona #}
|
||||
<li class="page-item {% if page >= total_pages %}disabled{% endif %}">
|
||||
<a class="page-link"
|
||||
href="{{ url_for('admin_receipts', id=id) }}?{{ query_string }}{% if query_string %}&{% endif %}page={{ page+1 }}">
|
||||
Następna »
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{% if orphan_files and request.path.endswith('/all') %}
|
||||
<hr class="my-4">
|
||||
|
@@ -4,7 +4,7 @@
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap mb-4">
|
||||
<h2 class="mb-2">👥 Zarządzanie użytkownikami</h2>
|
||||
<a href="/admin" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
<a href="{{ url_for('admin_panel') }}" class="btn btn-outline-secondary">← Powrót do panelu</a>
|
||||
</div>
|
||||
|
||||
<!-- Formularz dodawania nowego użytkownika -->
|
||||
|
Reference in New Issue
Block a user