zmiany uxowe w panelu
This commit is contained in:
32
app.py
32
app.py
@@ -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
|
||||
|
43
static/js/categories_autosave.js
Normal file
43
static/js/categories_autosave.js
Normal 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');
|
||||
})();
|
@@ -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>
|
||||
|
@@ -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 %}
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user