zmiany uxowe w panelu

This commit is contained in:
Mateusz Gruszczyński
2025-10-01 21:16:45 +02:00
parent 08b680f030
commit 365791cd35
6 changed files with 111 additions and 49 deletions

32
app.py
View File

@@ -3720,10 +3720,10 @@ def recalculate_filesizes_all():
return redirect(url_for("admin_receipts", id="all"))
@app.route("/admin/mass_edit_categories", methods=["GET", "POST"])
@app.route("/admin/edit_categories", methods=["GET", "POST"])
@login_required
@admin_required
def admin_mass_edit_categories():
def admin_edit_categories():
page, per_page = get_page_args(default_per_page=50, max_per_page=200)
lists_query = ShoppingList.query.options(
@@ -3752,13 +3752,13 @@ def admin_mass_edit_categories():
db.session.commit()
flash("Zaktualizowano kategorie dla wybranych list", "success")
return redirect(
url_for("admin_mass_edit_categories", page=page, per_page=per_page)
url_for("admin_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",
"admin/edit_categories.html",
lists=lists,
categories=categories,
page=page,
@@ -3768,6 +3768,30 @@ def admin_mass_edit_categories():
query_string=query_string,
)
@app.route("/admin/edit_categories/<int:list_id>/save", methods=["POST"])
@login_required
@admin_required
def admin_edit_categories_save(list_id):
l = db.session.get(ShoppingList, list_id)
if not l:
return jsonify(ok=False, error="not_found"), 404
data = request.get_json(silent=True) or {}
ids = data.get("category_ids", [])
try:
ids = [int(x) for x in ids]
except (TypeError, ValueError):
return jsonify(ok=False, error="bad_ids"), 400
l.categories.clear()
if ids:
cats = Category.query.filter(Category.id.in_(ids)).all()
l.categories.extend(cats)
db.session.commit()
return jsonify(ok=True, count=len(l.categories)), 200
@app.route("/admin/list_items/<int:list_id>")
@login_required

View File

@@ -0,0 +1,43 @@
(function () {
const $$ = (sel, ctx = document) => Array.from(ctx.querySelectorAll(sel));
const $ = (sel, ctx = document) => ctx.querySelector(sel);
const saveCategories = async (listId, ids, names, listTitle) => {
try {
const res = await fetch(`/admin/edit_categories/${listId}/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_ids: ids })
});
const data = await res.json().catch(() => ({}));
if (!res.ok || !data.ok) throw new Error(data.error || 'save_failed');
const cats = names.length ? names.join(', ') : 'brak';
showToast(`Zapisano kategorie [${cats}] dla listy <b>${listTitle}</b>`, 'success');
} catch (err) {
console.error('Autosave error:', err);
showToast(`Błąd zapisu kategorii dla listy <b>${listTitle}</b>`, 'danger');
}
};
const timers = new Map();
const debounce = (key, fn, delay = 300) => {
clearTimeout(timers.get(key));
timers.set(key, setTimeout(fn, delay));
};
$$('.form-select[name^="categories_"]').forEach(select => {
const listId = select.getAttribute('data-list-id') || select.name.replace('categories_', '');
const listTitle = select.closest('tr')?.querySelector('td a')?.textContent.trim() || `#${listId}`;
select.addEventListener('change', () => {
const selectedOptions = Array.from(select.options).filter(o => o.selected);
const ids = selectedOptions.map(o => o.value); // <-- ID
const names = selectedOptions.map(o => o.textContent.trim());
debounce(listId, () => saveCategories(listId, ids, names, listTitle));
});
});
const fallback = $('#fallback-save-btn');
if (fallback) fallback.classList.add('d-none');
})();

View File

@@ -13,7 +13,7 @@
<a href="{{ url_for('list_users') }}" class="btn btn-outline-light btn-sm">👥 Użytkownicy</a>
<a href="{{ url_for('admin_receipts') }}" class="btn btn-outline-light btn-sm">📸 Paragony</a>
<a href="{{ url_for('list_products') }}" class="btn btn-outline-light btn-sm">🛍️ Produkty</a>
<a href="{{ url_for('admin_mass_edit_categories') }}" class="btn btn-outline-light btn-sm">🗂 Kategorie</a>
<a href="{{ url_for('admin_edit_categories') }}" class="btn btn-outline-light btn-sm">🗂 Kategorie</a>
<a href="{{ url_for('admin_lists_access') }}" class="btn btn-outline-light btn-sm">🔐 Uprawnienia</a>
</div>
</div>

View File

@@ -12,25 +12,24 @@
<div class="card bg-dark text-white mb-5">
<div class="card-body">
<div class="alert alert-warning border-warning text-dark" role="alert">
⚠️ <strong>Uwaga!</strong> Przypisanie więcej niż jednej kategorii do listy może zaburzyć
poprawne zliczanie wydatków, ponieważ wydatki tej listy będą jednocześnie
klasyfikowane do kilku kategorii.
⚠️ <strong>Uwaga!</strong> Przypisanie więcej niż jednej kategorii do listy może zaburzyć poprawne zliczanie
wydatków.
</div>
<form method="post">
<div class="card bg-dark text-white mb-5">
<form method="post" id="mass-edit-form">
<div class="card bg-dark text-white mb-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-dark align-middle sortable">
<thead>
<div class="table-responsive" style="max-height: 70vh;">
<table class="table table-dark align-middle sortable mb-0">
<thead class="position-sticky top-0 bg-dark">
<tr>
<th scope="col">ID</th>
<th scope="col">Nazwa listy</th>
<th scope="col">Właściciel</th>
<th scope="col">Data utworzenia</th>
<th scope="col">Data</th>
<th scope="col">Status</th>
<th scope="col">Podgląd produktów</th>
<th scope="col">Kategorie</th>
<th scope="col">Podgląd</th>
<th scope="col" style="min-width: 260px;">Kategorie</th>
</tr>
</thead>
<tbody>
@@ -44,22 +43,17 @@
<td>
{% if l.owner %}
👤 {{ l.owner.username }} ({{ l.owner.id }})
{% else %}
-
{% endif %}
{% else %}-{% endif %}
</td>
<td>{{ l.created_at.strftime('%Y-%m-%d %H:%M') if l.created_at else '-' }}</td>
<td>
{% if l.is_archived %}<span
class="badge rounded-pill bg-secondary">Archiwalna</span>{%
endif %}
class="badge rounded-pill bg-secondary me-1">Archiwalna</span>{% endif %}
{% if l.is_temporary %}<span
class="badge rounded-pill bg-warning text-dark">Tymczasowa</span>{%
class="badge rounded-pill bg-warning text-dark me-1">Tymczasowa</span>{%
endif %}
{% if l.is_public %}<span
class="badge rounded-pill bg-success">Publiczna</span>{% else
%}
<span class="badge rounded-pill bg-dark">Prywatna</span>{% endif %}
{% if l.is_public %}<span class="badge rounded-pill bg-success">Publiczna</span>
{% else %}<span class="badge rounded-pill bg-dark">Prywatna</span>{% endif %}
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-light preview-btn"
@@ -67,24 +61,25 @@
🔍 Zobacz
</button>
</td>
<td style="min-width: 220px;">
<select name="categories_{{ l.id }}" multiple
class="form-select tom-dark bg-dark text-white border-secondary rounded">
{% for cat in categories %}
<option value="{{ cat.id }}" {% if cat in l.categories %}selected{% endif
%}>
{{ cat.name }}
</option>
{% endfor %}
</select>
<td>
<div class="d-flex align-items-center gap-2">
<select name="categories_{{ l.id }}" multiple
class="form-select tom-dark bg-dark text-white border-secondary rounded"
data-list-id="{{ l.id }}"
aria-label="Wybierz kategorie dla listy {{ l.id }}">
{% for cat in categories %}
<option value="{{ cat.id }}" {% if cat in l.categories %}selected{%
endif %}>{{ cat.name }}</option>
{% endfor %}
</select>
</div>
</td>
</tr>
{% endfor %}
{% if lists|length == 0 %}
<tr>
<td colspan="12" class="text-center py-4">
Brak list zakupowych do wyświetlenia
</td>
<td colspan="12" class="text-center py-4">Brak list zakupowych do wyświetlenia</td>
</tr>
{% endif %}
</tbody>
@@ -92,9 +87,9 @@
</div>
</div>
</div>
<div>
<button type="submit" class="btn btn-sm btn-outline-light">💾 Zapisz zmiany</button>
</div>
{# Fallback ukryty przez JS #}
<button type="submit" class="btn btn-sm btn-outline-light" id="fallback-save-btn">💾 Zapisz zmiany</button>
</form>
</div>
</div>
@@ -120,8 +115,7 @@
</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>
<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 %}">
@@ -132,7 +126,6 @@
</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">
@@ -150,7 +143,9 @@
</div>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static_bp.serve_js', filename='preview_list_modal.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='categories_select_admin.js') }}?v={{ APP_VERSION }}"></script>
<script src="{{ url_for('static_bp.serve_js', filename='categories_autosave.js') }}?v={{ APP_VERSION }}"></script>
{% endblock %}

View File

@@ -55,7 +55,7 @@
<input type="hidden" name="action" value="save_changes">
<div class="table-responsive">
<table class="table table-dark align-middle" id="listsTable">
<table class="table table-dark align-middle sortable" id="listsTable">
<thead class="align-middle">
<tr>
<th scope="col" style="width:36px;"></th>
@@ -77,7 +77,7 @@
<input type="hidden" name="visible_ids" value="{{ l.id }}">
</td>
<td class="text-nowrap">#{{ l.id }}</td>
<td class="text-nowrap">{{ l.id }}</td>
<td class="fw-bold align-middle">
<a href="{{ url_for('view_list', list_id=l.id) }}" class="text-white text-decoration-none">{{ l.title

View File

@@ -31,7 +31,7 @@
{% endif %}
{# --- Tom Select CSS tylko dla wybranych podstron --- #}
{% set substrings_tomselect = ['/edit_my_list', '/admin/edit_list', '/admin/mass_edit_categories'] %}
{% set substrings_tomselect = ['/edit_my_list', '/admin/edit_list', '/admin/edit_categories'] %}
{% if substrings_tomselect | select("in", request.path) | list | length > 0 %}
<link href="{{ url_for('static_bp.serve_css_lib', filename='tom-select.bootstrap5.min.css') }}?v={{ APP_VERSION }}"
rel="stylesheet">
@@ -126,7 +126,7 @@
<script src="{{ url_for('static_bp.serve_js_lib', filename='cropper.min.js') }}?v={{ APP_VERSION }}"></script>
{% endif %}
{% set substrings = ['/edit_my_list', '/admin/edit_list', '/admin/mass_edit_categories'] %}
{% set substrings = ['/edit_my_list', '/admin/edit_list', '/admin/edit_categories'] %}
{% if substrings | select("in", request.path) | list | length > 0 %}
<script
src="{{ url_for('static_bp.serve_js_lib', filename='tom-select.complete.min.js') }}?v={{ APP_VERSION }}"></script>